From 64216857c0edabd1e6bbadf7bb2e8873392d5a42 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Thu, 14 Dec 2023 15:59:15 +0100 Subject: [PATCH 01/44] [PromotionBundle] Deprecate PromotionCouponGeneratorInstructionInterface --- .../Command/GenerateCouponsCommand.php | 12 +++++------ .../CouponGenerationAmountValidator.php | 6 +++--- .../CouponGenerationAmountValidatorSpec.php | 6 +++--- .../PromotionCouponGeneratorInstruction.php | 20 +++++++++---------- ...ionCouponGeneratorInstructionInterface.php | 9 +++++++++ .../FailedGenerationExceptionSpec.php | 4 ++-- ...romotionCouponGeneratorInstructionSpec.php | 14 ++++++------- 7 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php b/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php index 5a3b78b07c..875332c59d 100644 --- a/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php +++ b/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php @@ -16,6 +16,7 @@ namespace Sylius\Bundle\PromotionBundle\Command; use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstruction; use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInterface; +use Sylius\Component\Promotion\Generator\ReadablePromotionCouponGeneratorInstructionInterface; use Sylius\Component\Promotion\Model\PromotionInterface; use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface; use Symfony\Component\Console\Command\Command; @@ -83,12 +84,11 @@ final class GenerateCouponsCommand extends Command return 0; } - public function getGeneratorInstructions(int $count, int $codeLength): PromotionCouponGeneratorInstructionInterface + public function getGeneratorInstructions(int $count, int $codeLength): ReadablePromotionCouponGeneratorInstructionInterface { - $instruction = new PromotionCouponGeneratorInstruction(); - $instruction->setAmount($count); - $instruction->setCodeLength($codeLength); - - return $instruction; + return new PromotionCouponGeneratorInstruction( + amount: $count, + codeLength: $codeLength, + ); } } diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CouponGenerationAmountValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/CouponGenerationAmountValidator.php index e2fa5bace9..881c79ca2b 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/CouponGenerationAmountValidator.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CouponGenerationAmountValidator.php @@ -15,7 +15,7 @@ namespace Sylius\Bundle\PromotionBundle\Validator; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CouponPossibleGenerationAmount; use Sylius\Component\Promotion\Generator\GenerationPolicyInterface; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; +use Sylius\Component\Promotion\Generator\ReadablePromotionCouponGeneratorInstructionInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; @@ -32,8 +32,8 @@ final class CouponGenerationAmountValidator extends ConstraintValidator return; } - /** @var PromotionCouponGeneratorInstructionInterface $value */ - Assert::isInstanceOf($value, PromotionCouponGeneratorInstructionInterface::class); + /** @var ReadablePromotionCouponGeneratorInstructionInterface $value */ + Assert::isInstanceOf($value, ReadablePromotionCouponGeneratorInstructionInterface::class); /** @var CouponPossibleGenerationAmount $constraint */ Assert::isInstanceOf($constraint, CouponPossibleGenerationAmount::class); diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php index 25a783c984..abb275c019 100644 --- a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php @@ -18,7 +18,7 @@ use Prophecy\Argument; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CouponPossibleGenerationAmount; use Sylius\Bundle\PromotionBundle\Validator\CouponGenerationAmountValidator; use Sylius\Component\Promotion\Generator\GenerationPolicyInterface; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; +use Sylius\Component\Promotion\Generator\ReadablePromotionCouponGeneratorInstructionInterface; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -42,7 +42,7 @@ final class CouponGenerationAmountValidatorSpec extends ObjectBehavior function it_adds_violation( ExecutionContextInterface $context, - PromotionCouponGeneratorInstructionInterface $instruction, + ReadablePromotionCouponGeneratorInstructionInterface $instruction, GenerationPolicyInterface $generationPolicy, ): void { $constraint = new CouponPossibleGenerationAmount(); @@ -58,7 +58,7 @@ final class CouponGenerationAmountValidatorSpec extends ObjectBehavior function it_does_not_add_violation( ExecutionContextInterface $context, - PromotionCouponGeneratorInstructionInterface $instruction, + ReadablePromotionCouponGeneratorInstructionInterface $instruction, GenerationPolicyInterface $generationPolicy, ): void { $constraint = new CouponPossibleGenerationAmount(); diff --git a/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstruction.php b/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstruction.php index 816f51c01f..2d2769170b 100644 --- a/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstruction.php +++ b/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstruction.php @@ -15,17 +15,15 @@ namespace Sylius\Component\Promotion\Generator; final class PromotionCouponGeneratorInstruction implements PromotionCouponGeneratorInstructionInterface { - private ?int $amount = 5; - - private ?string $prefix = null; - - private ?int $codeLength = 6; - - private ?string $suffix = null; - - private ?\DateTimeInterface $expiresAt = null; - - private ?int $usageLimit = null; + public function __construct( + private ?int $amount = 5, + private ?string $prefix = null, + private ?int $codeLength = 6, + private ?string $suffix = null, + private ?\DateTimeInterface $expiresAt = null, + private ?int $usageLimit = null + ) { + } public function getAmount(): ?int { diff --git a/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstructionInterface.php b/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstructionInterface.php index 41b0867be9..3ca7d799b1 100644 --- a/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstructionInterface.php +++ b/src/Sylius/Component/Promotion/Generator/PromotionCouponGeneratorInstructionInterface.php @@ -13,6 +13,15 @@ declare(strict_types=1); namespace Sylius\Component\Promotion\Generator; +trigger_deprecation( + 'sylius/promotion', + '1.13', + 'The "%s" interface is deprecated, use "%s" instead.', + PromotionCouponGeneratorInstructionInterface::class, + ReadablePromotionCouponGeneratorInstructionInterface::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see ReadablePromotionCouponGeneratorInstructionInterface} instead. */ interface PromotionCouponGeneratorInstructionInterface extends ReadablePromotionCouponGeneratorInstructionInterface { public function setAmount(?int $amount): void; diff --git a/src/Sylius/Component/Promotion/spec/Exception/FailedGenerationExceptionSpec.php b/src/Sylius/Component/Promotion/spec/Exception/FailedGenerationExceptionSpec.php index 16180a0dd9..9547e9d1bb 100644 --- a/src/Sylius/Component/Promotion/spec/Exception/FailedGenerationExceptionSpec.php +++ b/src/Sylius/Component/Promotion/spec/Exception/FailedGenerationExceptionSpec.php @@ -14,12 +14,12 @@ declare(strict_types=1); namespace spec\Sylius\Component\Promotion\Exception; use PhpSpec\ObjectBehavior; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; +use Sylius\Component\Promotion\Generator\ReadablePromotionCouponGeneratorInstructionInterface; final class FailedGenerationExceptionSpec extends ObjectBehavior { function let( - PromotionCouponGeneratorInstructionInterface $instruction, + ReadablePromotionCouponGeneratorInstructionInterface $instruction, \InvalidArgumentException $previousException, ): void { $instruction->getAmount()->willReturn(17); diff --git a/src/Sylius/Component/Promotion/spec/Generator/PromotionCouponGeneratorInstructionSpec.php b/src/Sylius/Component/Promotion/spec/Generator/PromotionCouponGeneratorInstructionSpec.php index ba91451e29..75d2d93040 100644 --- a/src/Sylius/Component/Promotion/spec/Generator/PromotionCouponGeneratorInstructionSpec.php +++ b/src/Sylius/Component/Promotion/spec/Generator/PromotionCouponGeneratorInstructionSpec.php @@ -14,13 +14,13 @@ declare(strict_types=1); namespace spec\Sylius\Component\Promotion\Generator; use PhpSpec\ObjectBehavior; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; +use Sylius\Component\Promotion\Generator\ReadablePromotionCouponGeneratorInstructionInterface; final class PromotionCouponGeneratorInstructionSpec extends ObjectBehavior { function it_implements_an_promotion_coupon_genarator_instruction_interface(): void { - $this->shouldImplement(PromotionCouponGeneratorInstructionInterface::class); + $this->shouldImplement(ReadablePromotionCouponGeneratorInstructionInterface::class); } function it_has_amount_equal_to_5_by_default(): void @@ -30,25 +30,25 @@ final class PromotionCouponGeneratorInstructionSpec extends ObjectBehavior function its_amount_should_be_mutable(): void { - $this->setAmount(500); + $this->beConstructedWith(500); $this->getAmount()->shouldReturn(500); } function its_prefix_is_mutable(): void { - $this->setPrefix('PREFIX_'); + $this->beConstructedWith(null, 'PREFIX_'); $this->getPrefix()->shouldReturn('PREFIX_'); } function its_code_length_is_mutable(): void { - $this->setCodeLength(4); + $this->beConstructedWith(null, null, 4); $this->getCodeLength()->shouldReturn(4); } function its_suffix_is_mutable(): void { - $this->setSuffix('_SUFFIX'); + $this->beConstructedWith(null, null, null, '_SUFFIX'); $this->getSuffix()->shouldReturn('_SUFFIX'); } @@ -59,7 +59,7 @@ final class PromotionCouponGeneratorInstructionSpec extends ObjectBehavior function its_usage_limit_is_mutable(): void { - $this->setUsageLimit(3); + $this->beConstructedWith(null, null, null, null, null, 3); $this->getUsageLimit()->shouldReturn(3); } } From ca504133564208c5073b306ff0be6384bf7b490a Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Thu, 14 Dec 2023 16:16:49 +0100 Subject: [PATCH 02/44] [PromotionBundle] Add validation group to PromotionCouponGeneratorInstructionType --- ...romotionCouponGeneratorInstructionType.php | 20 +++++++++++++++++-- .../Resources/config/services/forms.xml | 4 ++++ .../PromotionCouponGeneratorInstruction.xml | 5 +++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php index 8a7046307f..31e4cf87a5 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php @@ -13,14 +13,22 @@ declare(strict_types=1); namespace Sylius\Bundle\PromotionBundle\Form\Type; -use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; -final class PromotionCouponGeneratorInstructionType extends AbstractResourceType +final class PromotionCouponGeneratorInstructionType extends AbstractType { + /** @param array $validationGroups */ + public function __construct( + private string $dataClass, + private array $validationGroups = [] + ) { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder @@ -50,6 +58,14 @@ final class PromotionCouponGeneratorInstructionType extends AbstractResourceType ; } + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => $this->dataClass, + 'validation_groups' => $this->validationGroups, + ]); + } + public function getBlockPrefix(): string { return 'sylius_promotion_coupon_generator_instruction'; diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml index 5304e7553c..aa09ac7589 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml @@ -34,6 +34,9 @@ sylius + + sylius + @@ -129,6 +132,7 @@ Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstruction + %sylius.form.type.promotion_coupon_generator_instruction.validation_groups% diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml index 67cedc3a11..caa628f84d 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml @@ -17,16 +17,19 @@ + + + @@ -34,9 +37,11 @@ + + From 192f29ea578f9801591caee63fd31e89b6bccb44 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Mon, 18 Dec 2023 11:10:05 +0100 Subject: [PATCH 03/44] [PromotionBundle] Add sylius validation group to CouponPossibleGenerationAmount --- .../config/validation/PromotionCouponGeneratorInstruction.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml index caa628f84d..fbe8ec1068 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml @@ -13,7 +13,9 @@ - + + + From aed6f6461198b1875ced36e3a2d398ea598bf004 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Tue, 19 Dec 2023 08:25:18 +0100 Subject: [PATCH 04/44] [PromotionBundle] Add data mapper to PromotionCouponGeneratorInstructionType --- ...romotionCouponGeneratorInstructionType.php | 24 +++++++++++-- .../Resources/config/services.xml | 3 ++ .../Resources/config/services/forms.xml | 5 +++ ...otionCouponGeneratorInstructionFactory.php | 25 ++++++++++++++ ...onGeneratorInstructionFactoryInterface.php | 11 ++++++ ...nCouponGeneratorInstructionFactorySpec.php | 34 +++++++++++++++++++ 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/Sylius/Component/Promotion/Factory/PromotionCouponGeneratorInstructionFactory.php create mode 100644 src/Sylius/Component/Promotion/Factory/PromotionCouponGeneratorInstructionFactoryInterface.php create mode 100644 src/Sylius/Component/Promotion/spec/Factory/PromotionCouponGeneratorInstructionFactorySpec.php diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php index 31e4cf87a5..4e3b9bf52b 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php @@ -13,19 +13,23 @@ declare(strict_types=1); namespace Sylius\Bundle\PromotionBundle\Form\Type; +use Sylius\Component\Promotion\Factory\PromotionCouponGeneratorInstructionFactoryInterface; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -final class PromotionCouponGeneratorInstructionType extends AbstractType +final class PromotionCouponGeneratorInstructionType extends AbstractType implements DataMapperInterface { /** @param array $validationGroups */ public function __construct( + private DataMapperInterface $propertyPathDataMapper, + private PromotionCouponGeneratorInstructionFactoryInterface $promotionCouponGeneratorInstructionFactory, private string $dataClass, - private array $validationGroups = [] + private array $validationGroups = [], ) { } @@ -55,6 +59,7 @@ final class PromotionCouponGeneratorInstructionType extends AbstractType 'label' => 'sylius.form.promotion_coupon_generator_instruction.expires_at', 'widget' => 'single_text', ]) + ->setDataMapper($this) ; } @@ -66,6 +71,21 @@ final class PromotionCouponGeneratorInstructionType extends AbstractType ]); } + public function mapDataToForms($viewData, \Traversable $forms): void + { + $this->propertyPathDataMapper->mapDataToForms($viewData, $forms); + } + + public function mapFormsToData(\Traversable $forms, &$viewData): void + { + $data = []; + foreach ($forms as $form) { + $data[$form->getName()] = $form->getData(); + } + + $viewData = $this->promotionCouponGeneratorInstructionFactory->createFromArray($data); + } + public function getBlockPrefix(): string { return 'sylius_promotion_coupon_generator_instruction'; diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml index 54d7ad2bf5..3932aea428 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml @@ -28,6 +28,9 @@ + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml index aa09ac7589..cb329e3b03 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml @@ -131,9 +131,14 @@ + + + + Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstruction %sylius.form.type.promotion_coupon_generator_instruction.validation_groups% + diff --git a/src/Sylius/Component/Promotion/Factory/PromotionCouponGeneratorInstructionFactory.php b/src/Sylius/Component/Promotion/Factory/PromotionCouponGeneratorInstructionFactory.php new file mode 100644 index 0000000000..18e28d3d18 --- /dev/null +++ b/src/Sylius/Component/Promotion/Factory/PromotionCouponGeneratorInstructionFactory.php @@ -0,0 +1,25 @@ + $data */ + public function createFromArray(array $data): ReadablePromotionCouponGeneratorInstructionInterface; +} diff --git a/src/Sylius/Component/Promotion/spec/Factory/PromotionCouponGeneratorInstructionFactorySpec.php b/src/Sylius/Component/Promotion/spec/Factory/PromotionCouponGeneratorInstructionFactorySpec.php new file mode 100644 index 0000000000..ff33e898c7 --- /dev/null +++ b/src/Sylius/Component/Promotion/spec/Factory/PromotionCouponGeneratorInstructionFactorySpec.php @@ -0,0 +1,34 @@ + $now, + 'suffix' => 'suffix', + 'prefix' => 'prefix', + 'codeLength' => 10, + 'amount' => 1, + 'usageLimit' => 7, + ]; + $this->createFromArray($data)->shouldBeLike(new PromotionCouponGeneratorInstruction(1, 'prefix', 10, 'suffix', $now, 7)); + } +} From 1ac8eb69dfc5cf83ba766465c7ab24172caedd29 Mon Sep 17 00:00:00 2001 From: Sylius Bot Date: Mon, 18 Dec 2023 02:33:34 +0000 Subject: [PATCH 05/44] [CS][DX] Refactor --- .../Api/Admin/ManagingProductsContext.php | 2 +- .../Api/Admin/ManagingPromotionsContext.php | 2 +- .../Bundle/AdminBundle/Menu/MainMenuBuilder.php | 2 +- .../spec/Serializer/TaxRateDenormalizerSpec.php | 1 - .../Rule/ContainsProductConfigurationType.php | 2 -- .../Promotion/Rule/NthOrderConfigurationType.php | 2 -- .../Theme/ChannelBasedThemeContext.php | 2 +- .../Action/FixedDiscountConfigurationType.php | 2 -- .../PercentageDiscountConfigurationType.php | 3 --- .../UnitFixedDiscountConfigurationType.php | 2 -- .../UnitPercentageDiscountConfigurationType.php | 3 --- .../Type/Rule/CartQuantityConfigurationType.php | 2 -- .../Type/Rule/ItemTotalConfigurationType.php | 2 -- .../SyliusPromotionExtensionTest.php | 16 ++++++++-------- 14 files changed, 12 insertions(+), 31 deletions(-) diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index e1e3c9f2d6..151904ac6f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -562,7 +562,7 @@ final class ManagingProductsContext implements Context $this->responseChecker->hasItemWithValues($this->client->getLastResponse(), [ 'product' => $this->sectionAwareIriConverter->getIriFromResourceInSection($product, 'admin'), 'taxon' => $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), - ]) + ]), ); } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php index 943ae54483..8767f6cc22 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php @@ -836,7 +836,7 @@ final class ManagingPromotionsContext implements Context $returnedPromotion = current($this->responseChecker->getCollectionItemsWithValue( $this->client->getLastResponse(), 'code', - $promotion->getCode() + $promotion->getCode(), )); Assert::same( diff --git a/src/Sylius/Bundle/AdminBundle/Menu/MainMenuBuilder.php b/src/Sylius/Bundle/AdminBundle/Menu/MainMenuBuilder.php index 59675756b3..a3f0e4a8df 100644 --- a/src/Sylius/Bundle/AdminBundle/Menu/MainMenuBuilder.php +++ b/src/Sylius/Bundle/AdminBundle/Menu/MainMenuBuilder.php @@ -325,7 +325,7 @@ final class MainMenuBuilder { $configuration = $menu ->addChild('official_support') - ->setLabel('sylius.menu.admin.main.official_support.header' ) + ->setLabel('sylius.menu.admin.main.official_support.header') ; $configuration diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php index d3d91f3b6b..ffd47cee7b 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Serializer; use PhpSpec\ObjectBehavior; -use Sylius\Bundle\ApiBundle\Serializer\TaxRateDenormalizer; use Sylius\Component\Taxation\Model\TaxRateInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php index 8394a403de..3fb7c956f0 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php @@ -19,8 +19,6 @@ use Sylius\Component\Resource\Repository\RepositoryInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\ReversedTransformer; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class ContainsProductConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php index ca55416907..a4a9843370 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php @@ -16,8 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class NthOrderConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/CoreBundle/Theme/ChannelBasedThemeContext.php b/src/Sylius/Bundle/CoreBundle/Theme/ChannelBasedThemeContext.php index ea18a000d2..a7a50e9b4d 100644 --- a/src/Sylius/Bundle/CoreBundle/Theme/ChannelBasedThemeContext.php +++ b/src/Sylius/Bundle/CoreBundle/Theme/ChannelBasedThemeContext.php @@ -22,7 +22,7 @@ use Sylius\Component\Core\Model\ChannelInterface; final class ChannelBasedThemeContext implements ThemeContextInterface { - private null|false|ThemeInterface $theme = false; + private false|ThemeInterface|null $theme = false; public function __construct(private ChannelContextInterface $channelContext, private ThemeRepositoryInterface $themeRepository) { diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php index 6d7f39b885..acbbb7645f 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php @@ -17,8 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class FixedDiscountConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php index bb4980b475..5cb5f630ee 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\PromotionBundle\Form\Type\Action; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class PercentageDiscountConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php index ca4641dddf..7bd7b11fa5 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php @@ -18,8 +18,6 @@ use Sylius\Bundle\PromotionBundle\Form\Type\PromotionFilterCollectionType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class UnitFixedDiscountConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php index 8ea18fad36..29f605cb32 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php @@ -18,9 +18,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class UnitPercentageDiscountConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php index 82ea367b32..670764d5ff 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php @@ -16,8 +16,6 @@ namespace Sylius\Bundle\PromotionBundle\Form\Type\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class CartQuantityConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php index d868fa8d16..7c9137b5dc 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php @@ -17,8 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class ItemTotalConfigurationType extends AbstractType { diff --git a/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php b/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php index 14fedf09b6..dc7bd47dea 100644 --- a/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php +++ b/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php @@ -124,14 +124,14 @@ final class SyliusPromotionExtensionTest extends AbstractExtensionTestCase 'promotion_action' => [ 'validation_groups' => [ 'order_percentage_discount' => ['sylius', 'order_percentage_discount'], - 'order_fixed_discount' => ['sylius', 'order_fixed_discount'] - ] - ] + 'order_fixed_discount' => ['sylius', 'order_fixed_discount'], + ], + ], ]); $this->assertContainerBuilderHasParameter( 'sylius.promotion.promotion_action.validation_groups', - ['order_percentage_discount' => ['sylius', 'order_percentage_discount'], 'order_fixed_discount' => ['sylius', 'order_fixed_discount']] + ['order_percentage_discount' => ['sylius', 'order_percentage_discount'], 'order_fixed_discount' => ['sylius', 'order_fixed_discount']], ); } @@ -142,14 +142,14 @@ final class SyliusPromotionExtensionTest extends AbstractExtensionTestCase 'promotion_rule' => [ 'validation_groups' => [ 'cart_quantity' => ['sylius', 'cart_quantity'], - 'nth_order' => ['sylius', 'nth_order'] - ] - ] + 'nth_order' => ['sylius', 'nth_order'], + ], + ], ]); $this->assertContainerBuilderHasParameter( 'sylius.promotion.promotion_rule.validation_groups', - ['cart_quantity' => ['sylius', 'cart_quantity'], 'nth_order' => ['sylius', 'nth_order']] + ['cart_quantity' => ['sylius', 'cart_quantity'], 'nth_order' => ['sylius', 'nth_order']], ); } From 760727b00357d5a8beb4300c8db248d72adf7bd2 Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Mon, 18 Dec 2023 03:35:32 +0100 Subject: [PATCH 06/44] [Unit] Refactor OrderPlacerTrait --- tests/Api/Utils/OrderPlacerTrait.php | 184 +++++++++++++++++++++------ 1 file changed, 148 insertions(+), 36 deletions(-) diff --git a/tests/Api/Utils/OrderPlacerTrait.php b/tests/Api/Utils/OrderPlacerTrait.php index 13f07a7e33..afa9a65e40 100644 --- a/tests/Api/Utils/OrderPlacerTrait.php +++ b/tests/Api/Utils/OrderPlacerTrait.php @@ -21,67 +21,115 @@ use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder; use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart; use Sylius\Component\Core\Model\Address; use Sylius\Component\Core\Model\OrderInterface; +use Sylius\Component\Core\OrderPaymentTransitions; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Order\OrderTransitions; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\HandledStamp; use Webmozart\Assert\Assert; trait OrderPlacerTrait { - protected function placeOrder(string $tokenValue, string $email = 'sylius@example.com'): void + private MessageBusInterface $commandBus; + + private OrderRepositoryInterface $orderRepository; + + public function setUp(): void { - /** @var MessageBusInterface $commandBus */ - $commandBus = self::getContainer()->get('sylius.command_bus'); + parent::setUp(); - $pickupCartCommand = new PickupCart($tokenValue, 'en_US'); - $pickupCartCommand->setChannelCode('WEB'); - $commandBus->dispatch($pickupCartCommand); + $this->commandBus = self::getContainer()->get('sylius.command_bus'); + $this->orderRepository = $this->get('sylius.repository.order'); + } - $addItemToCartCommand = new AddItemToCart('MUG_BLUE', 3); - $addItemToCartCommand->setOrderTokenValue($tokenValue); - $commandBus->dispatch($addItemToCartCommand); + protected function fulfillOrder( + string $tokenValue, + string $productVariantCode = 'MUG_BLUE', + int $quantity = 3, + string $email = 'sylius@example.com', + ?\DateTimeImmutable $checkoutCompletedAt = null, + ): OrderInterface { + $this->pickUpCart($tokenValue); + $this->addItemToCart($productVariantCode, $quantity, $tokenValue); + $cart = $this->updateCartWithAddress($tokenValue, $email); + $this->dispatchShippingMethodChooseCommand($tokenValue, 'UPS', (string) $cart->getShipments()->first()->getId()); + $this->dispatchPaymentMethodChooseCommand( + $tokenValue, + 'CASH_ON_DELIVERY', + (string) $cart->getLastPayment()->getId(), + ); + $order = $this->dispatchCompleteOrderCommand($tokenValue); + $this->payOrder($order); + $this->setCheckoutCompletedAt($order, $checkoutCompletedAt); - $address = new Address(); - $address->setFirstName('John'); - $address->setLastName('Doe'); - $address->setCity('New York'); - $address->setStreet('Avenue'); - $address->setCountryCode('US'); - $address->setPostcode('90000'); + return $order; + } - $updateCartCommand = new UpdateCart($email, $address); - $updateCartCommand->setOrderTokenValue($tokenValue); - $commandBus->dispatch($updateCartCommand); + protected function placeOrder( + string $tokenValue, + string $email = 'sylius@example.com', + string $productVariantCode = 'MUG_BLUE', + int $quantity = 3, + ?\DateTimeImmutable $checkoutCompletedAt = null, + ): OrderInterface { + $this->pickUpCart($tokenValue); + $this->addItemToCart($productVariantCode, $quantity, $tokenValue); + $cart = $this->updateCartWithAddress($tokenValue, $email); + $this->dispatchShippingMethodChooseCommand($tokenValue, 'UPS', (string) $cart->getShipments()->first()->getId()); + $this->dispatchPaymentMethodChooseCommand( + $tokenValue, + 'CASH_ON_DELIVERY', + (string) $cart->getLastPayment()->getId(), + ); - /** @var OrderRepositoryInterface $orderRepository */ - $orderRepository = $this->get('sylius.repository.order'); - /** @var OrderInterface|null $cart */ - $cart = $orderRepository->findCartByTokenValue($tokenValue); - Assert::notNull($cart); + $order = $this->dispatchCompleteOrderCommand($tokenValue); - $chooseShippingMethodCommand = new ChooseShippingMethod('UPS'); + return $order; + } + + private function dispatchShippingMethodChooseCommand( + string $tokenValue, + string $shippingMethodCode = 'UPS', + ?string $subresourceId = null, + ): OrderInterface { + $chooseShippingMethodCommand = new ChooseShippingMethod($shippingMethodCode); $chooseShippingMethodCommand->setOrderTokenValue($tokenValue); - $chooseShippingMethodCommand->setSubresourceId((string) $cart->getShipments()->first()->getId()); - $commandBus->dispatch($chooseShippingMethodCommand); + $chooseShippingMethodCommand->setSubresourceId($subresourceId); - $choosePaymentMethodCommand = new ChoosePaymentMethod('CASH_ON_DELIVERY'); + $envelope = $this->commandBus->dispatch($chooseShippingMethodCommand); + + return $envelope->last(HandledStamp::class)->getResult(); + } + + private function dispatchPaymentMethodChooseCommand( + string $tokenValue, + string $paymentMethodCode = 'CASH_ON_DELIVERY', + ?string $subresourceId = null, + ): OrderInterface { + $choosePaymentMethodCommand = new ChoosePaymentMethod($paymentMethodCode); $choosePaymentMethodCommand->setOrderTokenValue($tokenValue); - $choosePaymentMethodCommand->setSubresourceId((string) $cart->getLastPayment()->getId()); - $commandBus->dispatch($choosePaymentMethodCommand); + $choosePaymentMethodCommand->setSubresourceId($subresourceId); + $envelope = $this->commandBus->dispatch($choosePaymentMethodCommand); + + return $envelope->last(HandledStamp::class)->getResult(); + } + + protected function dispatchCompleteOrderCommand( + string $tokenValue, + ): OrderInterface { $completeOrderCommand = new CompleteOrder(); $completeOrderCommand->setOrderTokenValue($tokenValue); - $commandBus->dispatch($completeOrderCommand); + $envelope = $this->commandBus->dispatch($completeOrderCommand); + + return $envelope->last(HandledStamp::class)->getResult(); } protected function cancelOrder(string $tokenValue): void { $objectManager = $this->get('doctrine.orm.entity_manager'); - /** @var OrderRepositoryInterface $orderRepository */ - $orderRepository = $this->get('sylius.repository.order'); - /** @var OrderInterface|null $order */ - $order = $orderRepository->findOneByTokenValue($tokenValue); + $order = $this->orderRepository->findOneByTokenValue($tokenValue); Assert::notNull($order); $stateMachineFactory = $this->get('sm.factory'); @@ -90,6 +138,70 @@ trait OrderPlacerTrait $stateMachine->apply(OrderTransitions::TRANSITION_CANCEL); $objectManager->flush(); - $objectManager->clear(); + } + + protected function payOrder(OrderInterface $order): OrderInterface + { + $objectManager = $this->get('doctrine.orm.entity_manager'); + + $stateMachineFactory = $this->get('sm.factory'); + + $stateMachine = $stateMachineFactory->get($order, OrderPaymentTransitions::GRAPH); + $stateMachine->apply(OrderPaymentTransitions::TRANSITION_PAY); + + $objectManager->flush(); + + return $order; + } + + private function setCheckoutCompletedAt( + OrderInterface $order, + ?\DateTimeImmutable $checkoutCompletedAt, + ): OrderInterface { + $objectManager = $this->get('doctrine.orm.entity_manager'); + + $order->setCheckoutCompletedAt($checkoutCompletedAt); + + $objectManager->flush(); + + return $order; + } + + protected function pickUpCart(string $tokenValue = 'nAWw2jewpA', string $channelCode = 'WEB'): string + { + $pickupCartCommand = new PickupCart($tokenValue); + $pickupCartCommand->setChannelCode($channelCode); + + $this->commandBus->dispatch($pickupCartCommand); + + return $tokenValue; + } + + protected function addItemToCart(string $productVariantCode, int $quantity, string $tokenValue): string + { + $addItemToCartCommand = new AddItemToCart($productVariantCode, $quantity); + $addItemToCartCommand->setOrderTokenValue($tokenValue); + + $this->commandBus->dispatch($addItemToCartCommand); + + return $tokenValue; + } + + protected function updateCartWithAddress(string $tokenValue, string $email = 'sylius@example.com'): OrderInterface + { + $address = new Address(); + $address->setFirstName('John'); + $address->setLastName('Doe'); + $address->setCity('New York'); + $address->setStreet('Avenue'); + $address->setCountryCode('US'); + $address->setPostcode('90000'); + + $updateCartCommand = new UpdateCart(email: $email, billingAddress: $address); + $updateCartCommand->setOrderTokenValue($tokenValue); + + $envelope = $this->commandBus->dispatch($updateCartCommand); + + return $envelope->last(HandledStamp::class)->getResult(); } } From 3cf271135059c95270bdbd3705b093fd88420821 Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Mon, 18 Dec 2023 03:36:49 +0100 Subject: [PATCH 07/44] [Unit] Refactor tests that uses OrderPlacerTrait --- tests/Api/Shop/CheckoutCompletionTest.php | 79 ++--------------------- tests/Api/Shop/OrdersTest.php | 58 +---------------- 2 files changed, 9 insertions(+), 128 deletions(-) diff --git a/tests/Api/Shop/CheckoutCompletionTest.php b/tests/Api/Shop/CheckoutCompletionTest.php index 779f4d28ac..8228dee1b3 100644 --- a/tests/Api/Shop/CheckoutCompletionTest.php +++ b/tests/Api/Shop/CheckoutCompletionTest.php @@ -13,20 +13,14 @@ declare(strict_types=1); namespace Sylius\Tests\Api\Shop; -use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart; -use Sylius\Bundle\ApiBundle\Command\Cart\PickupCart; -use Sylius\Bundle\ApiBundle\Command\Checkout\ChoosePaymentMethod; -use Sylius\Bundle\ApiBundle\Command\Checkout\ChooseShippingMethod; -use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart; -use Sylius\Component\Core\Model\Address; use Sylius\Tests\Api\JsonApiTestCase; +use Sylius\Tests\Api\Utils\OrderPlacerTrait; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Messenger\MessageBusInterface; use Webmozart\Assert\Assert; final class CheckoutCompletionTest extends JsonApiTestCase { - private MessageBusInterface $commandBus; + use OrderPlacerTrait; /** @test */ public function it_prevents_from_order_completion_if_order_is_in_the_cart_state(): void @@ -83,7 +77,7 @@ final class CheckoutCompletionTest extends JsonApiTestCase $tokenValue = $this->pickUpCart(); $this->addItemToCart('MUG_BLUE', 3, $tokenValue); $this->updateCartWithAddress($tokenValue); - $this->chooseShippingMethod($tokenValue, $this->getFirstShipmentId($tokenValue)); + $this->dispatchShippingMethodChooseCommand($tokenValue, 'DHL', $this->getFirstShipmentId($tokenValue)); $this->client->request( method: 'PATCH', @@ -133,8 +127,8 @@ final class CheckoutCompletionTest extends JsonApiTestCase $this->addItemToCart('MUG_BLUE', 3, $tokenValue); $this->addItemToCart('MUG_NFT', 1, $tokenValue); $this->updateCartWithAddress($tokenValue); - $this->chooseShippingMethod($tokenValue, $this->getFirstShipmentId($tokenValue)); - $this->choosePaymentMethod($tokenValue, $this->getFirstPaymentId($tokenValue)); + $this->dispatchShippingMethodChooseCommand($tokenValue, 'DHL', $this->getFirstShipmentId($tokenValue)); + $this->dispatchPaymentMethodChooseCommand($tokenValue, 'BANK_TRANSFER', $this->getFirstPaymentId($tokenValue)); $this->client->request( method: 'PATCH', @@ -159,7 +153,7 @@ final class CheckoutCompletionTest extends JsonApiTestCase $tokenValue = $this->pickUpCart(); $this->addItemToCart('MUG_NFT', 1, $tokenValue); $this->updateCartWithAddress($tokenValue); - $this->choosePaymentMethod($tokenValue, $this->getFirstPaymentId($tokenValue)); + $this->dispatchPaymentMethodChooseCommand($tokenValue, 'BANK_TRANSFER', $this->getFirstPaymentId($tokenValue)); $this->client->request( method: 'PATCH', @@ -200,67 +194,6 @@ final class CheckoutCompletionTest extends JsonApiTestCase ); } - protected function setUp(): void - { - parent::setUp(); - - $this->commandBus = self::getContainer()->get('sylius.command_bus'); - } - - private function pickUpCart(): string - { - $tokenValue = 'nAWw2jewpA'; - - $pickupCartCommand = new PickupCart($tokenValue); - $pickupCartCommand->setChannelCode('WEB'); - - $this->commandBus->dispatch($pickupCartCommand); - - return $tokenValue; - } - - private function addItemToCart(string $productVariantCode, int $quantity, string $tokenValue): void - { - $addItemToCartCommand = new AddItemToCart($productVariantCode, $quantity); - $addItemToCartCommand->setOrderTokenValue($tokenValue); - - $this->commandBus->dispatch($addItemToCartCommand); - } - - private function updateCartWithAddress(string $tokenValue): void - { - $address = new Address(); - $address->setFirstName('John'); - $address->setLastName('Doe'); - $address->setCity('New York'); - $address->setStreet('Avenue'); - $address->setCountryCode('US'); - $address->setPostcode('90000'); - - $updateCartCommand = new UpdateCart(email: 'sylius@example.com', billingAddress: $address); - $updateCartCommand->setOrderTokenValue($tokenValue); - - $this->commandBus->dispatch($updateCartCommand); - } - - private function chooseShippingMethod(string $tokenValue, string $shipmentId): void - { - $chooseShippingMethodCommand = new ChooseShippingMethod('DHL'); - $chooseShippingMethodCommand->setSubresourceId($shipmentId); - $chooseShippingMethodCommand->setOrderTokenValue($tokenValue); - - $this->commandBus->dispatch($chooseShippingMethodCommand); - } - - private function choosePaymentMethod(string $tokenValue, string $paymentId): void - { - $choosePaymentMethodCommand = new ChoosePaymentMethod('BANK_TRANSFER'); - $choosePaymentMethodCommand->setSubresourceId($paymentId); - $choosePaymentMethodCommand->setOrderTokenValue($tokenValue); - - $this->commandBus->dispatch($choosePaymentMethodCommand); - } - private function getFirstShipmentId(string $tokenValue): string { $this->client->request( diff --git a/tests/Api/Shop/OrdersTest.php b/tests/Api/Shop/OrdersTest.php index bcf6bb2fc1..e2e68a58c9 100644 --- a/tests/Api/Shop/OrdersTest.php +++ b/tests/Api/Shop/OrdersTest.php @@ -13,26 +13,20 @@ declare(strict_types=1); namespace Sylius\Tests\Api\Shop; -use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart; use Sylius\Bundle\ApiBundle\Command\Cart\PickupCart; -use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart; use Sylius\Component\Addressing\Model\CountryInterface; -use Sylius\Component\Core\Model\Address; use Sylius\Component\Core\Model\AdjustmentInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Tests\Api\JsonApiTestCase; use Sylius\Tests\Api\Utils\OrderPlacerTrait; use Sylius\Tests\Api\Utils\ShopUserLoginTrait; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Messenger\MessageBusInterface; final class OrdersTest extends JsonApiTestCase { use ShopUserLoginTrait; use OrderPlacerTrait; - private MessageBusInterface $commandBus; - /** @test */ public function it_gets_an_order(): void { @@ -404,12 +398,9 @@ final class OrdersTest extends JsonApiTestCase $tokenValue = 'nAWw2jewpA'; - /** @var MessageBusInterface $commandBus */ - $commandBus = self::getContainer()->get('sylius.command_bus'); - $pickupCartCommand = new PickupCart($tokenValue); $pickupCartCommand->setChannelCode('WEB'); - $commandBus->dispatch($pickupCartCommand); + $this->commandBus->dispatch($pickupCartCommand); $this->client->request( method: 'POST', @@ -436,7 +427,7 @@ final class OrdersTest extends JsonApiTestCase $pickupCartCommand = new PickupCart($tokenValue); $pickupCartCommand->setChannelCode('WEB'); - $commandBus->dispatch($pickupCartCommand); + $this->commandBus->dispatch($pickupCartCommand); $this->client->request( method: 'POST', @@ -463,7 +454,7 @@ final class OrdersTest extends JsonApiTestCase $pickupCartCommand = new PickupCart($tokenValue); $pickupCartCommand->setChannelCode('WEB'); - $commandBus->dispatch($pickupCartCommand); + $this->commandBus->dispatch($pickupCartCommand); $this->client->request( method: 'PATCH', @@ -527,47 +518,4 @@ final class OrdersTest extends JsonApiTestCase $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND); } - - protected function setUp(): void - { - parent::setUp(); - - $this->commandBus = self::getContainer()->get('sylius.command_bus'); - } - - private function pickUpCart(): string - { - $tokenValue = 'nAWw2jewpA'; - - $pickupCartCommand = new PickupCart($tokenValue); - $pickupCartCommand->setChannelCode('WEB'); - - $this->commandBus->dispatch($pickupCartCommand); - - return $tokenValue; - } - - private function addItemToCart(string $productVariantCode, int $quantity, string $tokenValue): void - { - $addItemToCartCommand = new AddItemToCart($productVariantCode, $quantity); - $addItemToCartCommand->setOrderTokenValue($tokenValue); - - $this->commandBus->dispatch($addItemToCartCommand); - } - - private function updateCartWithAddress(string $tokenValue): void - { - $address = new Address(); - $address->setFirstName('John'); - $address->setLastName('Doe'); - $address->setCity('New York'); - $address->setStreet('Avenue'); - $address->setCountryCode('US'); - $address->setPostcode('90000'); - - $updateCartCommand = new UpdateCart(email: 'sylius@example.com', billingAddress: $address); - $updateCartCommand->setOrderTokenValue($tokenValue); - - $this->commandBus->dispatch($updateCartCommand); - } } From 538a43683824920c912b1bf03385549494d3ee6b Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Wed, 20 Dec 2023 09:58:38 +0100 Subject: [PATCH 08/44] [Unit] Add setUpOrderPlacer method in OrderPlacerTrait --- tests/Api/Admin/OrdersTest.php | 7 +++++ tests/Api/Admin/PaymentsTest.php | 7 +++++ tests/Api/Shop/CheckoutCompletionTest.php | 7 +++++ tests/Api/Shop/OrdersTest.php | 7 +++++ tests/Api/Shop/PaymentsTest.php | 7 +++++ tests/Api/Utils/OrderPlacerTrait.php | 31 +++++++++++++++++++---- 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/tests/Api/Admin/OrdersTest.php b/tests/Api/Admin/OrdersTest.php index 66c7ea574f..689faca3ba 100644 --- a/tests/Api/Admin/OrdersTest.php +++ b/tests/Api/Admin/OrdersTest.php @@ -24,6 +24,13 @@ final class OrdersTest extends JsonApiTestCase { use OrderPlacerTrait; + protected function setUp(): void + { + $this->setUpOrderPlacer(); + + parent::setUp(); + } + /** @test */ public function it_gets_an_order(): void { diff --git a/tests/Api/Admin/PaymentsTest.php b/tests/Api/Admin/PaymentsTest.php index b50351905f..ab3f0dfd81 100644 --- a/tests/Api/Admin/PaymentsTest.php +++ b/tests/Api/Admin/PaymentsTest.php @@ -23,6 +23,13 @@ final class PaymentsTest extends JsonApiTestCase use AdminUserLoginTrait; use OrderPlacerTrait; + protected function setUp(): void + { + $this->setUpOrderPlacer(); + + parent::setUp(); + } + /** @test */ public function it_gets_an_order(): void { diff --git a/tests/Api/Shop/CheckoutCompletionTest.php b/tests/Api/Shop/CheckoutCompletionTest.php index 8228dee1b3..af10db3b86 100644 --- a/tests/Api/Shop/CheckoutCompletionTest.php +++ b/tests/Api/Shop/CheckoutCompletionTest.php @@ -22,6 +22,13 @@ final class CheckoutCompletionTest extends JsonApiTestCase { use OrderPlacerTrait; + protected function setUp(): void + { + $this->setUpOrderPlacer(); + + parent::setUp(); + } + /** @test */ public function it_prevents_from_order_completion_if_order_is_in_the_cart_state(): void { diff --git a/tests/Api/Shop/OrdersTest.php b/tests/Api/Shop/OrdersTest.php index e2e68a58c9..6f427d8ab4 100644 --- a/tests/Api/Shop/OrdersTest.php +++ b/tests/Api/Shop/OrdersTest.php @@ -27,6 +27,13 @@ final class OrdersTest extends JsonApiTestCase use ShopUserLoginTrait; use OrderPlacerTrait; + protected function setUp(): void + { + $this->setUpOrderPlacer(); + + parent::setUp(); + } + /** @test */ public function it_gets_an_order(): void { diff --git a/tests/Api/Shop/PaymentsTest.php b/tests/Api/Shop/PaymentsTest.php index 850a217342..695a15a19d 100644 --- a/tests/Api/Shop/PaymentsTest.php +++ b/tests/Api/Shop/PaymentsTest.php @@ -23,6 +23,13 @@ final class PaymentsTest extends JsonApiTestCase use ShopUserLoginTrait; use OrderPlacerTrait; + protected function setUp(): void + { + $this->setUpOrderPlacer(); + + parent::setUp(); + } + /** @test */ public function it_gets_payment_from_placed_order(): void { diff --git a/tests/Api/Utils/OrderPlacerTrait.php b/tests/Api/Utils/OrderPlacerTrait.php index afa9a65e40..5f0d15849b 100644 --- a/tests/Api/Utils/OrderPlacerTrait.php +++ b/tests/Api/Utils/OrderPlacerTrait.php @@ -34,12 +34,14 @@ trait OrderPlacerTrait private OrderRepositoryInterface $orderRepository; - public function setUp(): void - { - parent::setUp(); + private bool $isSetUpOrderPlacerCalled = false; + final public function setUpOrderPlacer(): void + { $this->commandBus = self::getContainer()->get('sylius.command_bus'); $this->orderRepository = $this->get('sylius.repository.order'); + + $this->isSetUpOrderPlacerCalled = true; } protected function fulfillOrder( @@ -49,10 +51,16 @@ trait OrderPlacerTrait string $email = 'sylius@example.com', ?\DateTimeImmutable $checkoutCompletedAt = null, ): OrderInterface { + $this->checkSetUpOrderPlacerCalled(); + $this->pickUpCart($tokenValue); $this->addItemToCart($productVariantCode, $quantity, $tokenValue); $cart = $this->updateCartWithAddress($tokenValue, $email); - $this->dispatchShippingMethodChooseCommand($tokenValue, 'UPS', (string) $cart->getShipments()->first()->getId()); + $this->dispatchShippingMethodChooseCommand( + $tokenValue, + 'UPS', + (string) $cart->getShipments()->first()->getId(), + ); $this->dispatchPaymentMethodChooseCommand( $tokenValue, 'CASH_ON_DELIVERY', @@ -72,10 +80,16 @@ trait OrderPlacerTrait int $quantity = 3, ?\DateTimeImmutable $checkoutCompletedAt = null, ): OrderInterface { + $this->checkSetUpOrderPlacerCalled(); + $this->pickUpCart($tokenValue); $this->addItemToCart($productVariantCode, $quantity, $tokenValue); $cart = $this->updateCartWithAddress($tokenValue, $email); - $this->dispatchShippingMethodChooseCommand($tokenValue, 'UPS', (string) $cart->getShipments()->first()->getId()); + $this->dispatchShippingMethodChooseCommand( + $tokenValue, + 'UPS', + (string) $cart->getShipments()->first()->getId(), + ); $this->dispatchPaymentMethodChooseCommand( $tokenValue, 'CASH_ON_DELIVERY', @@ -204,4 +218,11 @@ trait OrderPlacerTrait return $envelope->last(HandledStamp::class)->getResult(); } + + private function checkSetUpOrderPlacerCalled(): void + { + if (!$this->isSetUpOrderPlacerCalled) { + throw new \LogicException('The setUpOrderPlacer() method must be called in setUp() method.'); + } + } } From 7af279a6a6b11ebdf17d7b52f12462984e582c02 Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Mon, 4 Dec 2023 16:34:21 +0100 Subject: [PATCH 09/44] [Behat][API] Add new method to the ResponseCheckerInterface --- src/Sylius/Behat/Client/ResponseChecker.php | 44 +++++++++++++++++-- .../Behat/Client/ResponseCheckerInterface.php | 4 +- .../Admin/ManagingProductImagesContext.php | 4 +- .../Api/Admin/ManagingTaxonImagesContext.php | 4 +- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/Sylius/Behat/Client/ResponseChecker.php b/src/Sylius/Behat/Client/ResponseChecker.php index 2ecfc0b082..5ddd6a5abc 100644 --- a/src/Sylius/Behat/Client/ResponseChecker.php +++ b/src/Sylius/Behat/Client/ResponseChecker.php @@ -132,11 +132,31 @@ final class ResponseChecker implements ResponseCheckerInterface return false; } - /** @param string|int $value */ - public function hasSubResourceWithValue(Response $response, string $subResource, string $key, $value): bool - { + public function hasValueInAnySubresourceObjectCollection( + Response $response, + string $subResource, + string $key, + int|string $expectedValue, + ): bool { foreach ($this->getResponseContentValue($response, $subResource) as $resource) { - if ($resource[$key] === $value) { + if (!is_array($resource)) { + throw new \InvalidArgumentException(sprintf( + 'Expected subresource "%s" to be an array, got "%s"', + $subResource, + gettype($resource), + )); + } + + if (!array_key_exists($key, $resource)) { + throw new \InvalidArgumentException(sprintf( + 'Expected subresource "%s" to have key "%s", has these keys instead: "%s"', + $subResource, + $key, + implode(', ', array_keys($resource)), + )); + } + + if ($resource[$key] === $expectedValue) { return true; } } @@ -144,6 +164,22 @@ final class ResponseChecker implements ResponseCheckerInterface return false; } + public function hasValueInSubresourceObject(Response $response, string $subResource, string $key, int|string $expectedValue): bool + { + $resource = $this->getResponseContentValue($response, $subResource); + + if (array_key_exists($key, $resource)) { + return $resource[$key] === $expectedValue; + } + + throw new \InvalidArgumentException(sprintf( + 'Expected subresource "%s" to have key "%s", has these keys instead: "%s"', + $subResource, + $key, + implode(', ', array_keys($resource)), + )); + } + /** @param string|array $value */ public function hasItemOnPositionWithValue(Response $response, int $position, string $key, $value): bool { diff --git a/src/Sylius/Behat/Client/ResponseCheckerInterface.php b/src/Sylius/Behat/Client/ResponseCheckerInterface.php index c5f1b6c222..5d4a3a1ef0 100644 --- a/src/Sylius/Behat/Client/ResponseCheckerInterface.php +++ b/src/Sylius/Behat/Client/ResponseCheckerInterface.php @@ -51,7 +51,9 @@ interface ResponseCheckerInterface public function hasItemWithValue(Response $response, string $key, int|string $value): bool; - public function hasSubResourceWithValue(Response $response, string $subResource, string $key, int|string $value): bool; + public function hasValueInAnySubresourceObjectCollection(Response $response, string $subResource, string $key, int|string $expectedValue): bool; + + public function hasValueInSubresourceObject(Response $response, string $subResource, string $key, int|string $expectedValue): bool; public function hasItemOnPositionWithValue(Response $response, int $position, string $key, array|string $value): bool; diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php index a7c7acefa2..056651f779 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php @@ -131,7 +131,7 @@ final class ManagingProductImagesContext implements Context */ public function theProductShouldHaveAnImageWithType(ProductInterface $product, string $type): void { - Assert::true($this->responseChecker->hasSubResourceWithValue( + Assert::true($this->responseChecker->hasValueInAnySubresourceObjectCollection( $this->client->show(Resources::PRODUCTS, $product->getCode()), 'images', 'type', @@ -162,7 +162,7 @@ final class ManagingProductImagesContext implements Context */ public function thisProductShouldNotHaveAnyImagesWithType(ProductInterface $product, string $type): void { - Assert::false($this->responseChecker->hasSubResourceWithValue( + Assert::false($this->responseChecker->hasValueInAnySubresourceObjectCollection( $this->client->show(Resources::PRODUCTS, $product->getCode()), 'images', 'type', diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php index af08d62553..9d0bc33612 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php @@ -124,7 +124,7 @@ final class ManagingTaxonImagesContext implements Context */ public function thisTaxonShouldHaveAnImageWithType(TaxonInterface $taxon, string $type): void { - Assert::true($this->responseChecker->hasSubResourceWithValue( + Assert::true($this->responseChecker->hasValueInAnySubresourceObjectCollection( $this->client->show(Resources::TAXONS, $taxon->getCode()), 'images', 'type', @@ -138,7 +138,7 @@ final class ManagingTaxonImagesContext implements Context */ public function thisTaxonShouldNotHaveAnyImagesWithType(TaxonInterface $taxon, string $type): void { - Assert::false($this->responseChecker->hasSubResourceWithValue( + Assert::false($this->responseChecker->hasValueInAnySubresourceObjectCollection( $this->client->show(Resources::TAXONS, $taxon->getCode()), 'images', 'type', From e70347cdd15b881f482edd4b741a894b5eb18e60 Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Thu, 14 Dec 2023 02:50:57 +0100 Subject: [PATCH 10/44] [Behat] Add ChannelContext improvements --- .../Behat/Context/Setup/ChannelContext.php | 35 +++++++++---------- .../config/services/contexts/setup.xml | 1 - 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/Sylius/Behat/Context/Setup/ChannelContext.php b/src/Sylius/Behat/Context/Setup/ChannelContext.php index 063fa4bf69..5cbb00903f 100644 --- a/src/Sylius/Behat/Context/Setup/ChannelContext.php +++ b/src/Sylius/Behat/Context/Setup/ChannelContext.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Setup; use Behat\Behat\Context\Context; +use Behat\Step\Given; use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Service\Setter\ChannelContextSetterInterface; use Sylius\Behat\Service\SharedStorageInterface; @@ -26,10 +27,12 @@ use Sylius\Component\Core\Model\ShopBillingData; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Core\Test\Services\DefaultChannelFactoryInterface; use Sylius\Component\Locale\Model\LocaleInterface; -use Sylius\Component\Resource\Repository\RepositoryInterface; final class ChannelContext implements Context { + /** + * @param ChannelRepositoryInterface $channelRepository + */ public function __construct( private SharedStorageInterface $sharedStorage, private ChannelContextSetterInterface $channelContextSetter, @@ -37,7 +40,6 @@ final class ChannelContext implements Context private DefaultChannelFactoryInterface $defaultChannelFactory, private ChannelRepositoryInterface $channelRepository, private ObjectManager $channelManager, - private RepositoryInterface $localeRepository, ) { } @@ -69,7 +71,7 @@ final class ChannelContext implements Context /** * @Given the store operates on a single channel in "United States" */ - public function storeOperatesOnASingleChannelInUnitedStates() + public function storeOperatesOnASingleChannelInUnitedStates(): void { $defaultData = $this->unitedStatesChannelFactory->create(); @@ -78,9 +80,9 @@ final class ChannelContext implements Context } /** - * @Given the store operates on a single channel in the "United States" named :channelIdentifier + * @Given the store operates on a single channel in the "United States" named :channelName */ - public function storeOperatesOnASingleChannelInTheUnitedStatesNamed(string $channelName) + public function storeOperatesOnASingleChannelInTheUnitedStatesNamed(string $channelName): void { $channelCode = StringInflector::nameToLowercaseCode($channelName); $defaultData = $this->unitedStatesChannelFactory->create($channelCode, $channelName); @@ -93,7 +95,7 @@ final class ChannelContext implements Context * @Given the store operates on a single channel * @Given the store operates on a single channel in :currencyCode currency */ - public function storeOperatesOnASingleChannel($currencyCode = null) + public function storeOperatesOnASingleChannel(?string $currencyCode = null): void { $defaultData = $this->defaultChannelFactory->create(null, null, $currencyCode); @@ -137,7 +139,7 @@ final class ChannelContext implements Context /** * @Given the channel :channel is enabled */ - public function theChannelIsEnabled(ChannelInterface $channel) + public function theChannelIsEnabled(ChannelInterface $channel): void { $this->changeChannelState($channel, true); } @@ -146,7 +148,7 @@ final class ChannelContext implements Context * @Given the channel :channel is disabled * @Given the channel :channel has been disabled */ - public function theChannelIsDisabled(ChannelInterface $channel) + public function theChannelIsDisabled(ChannelInterface $channel): void { $this->changeChannelState($channel, false); } @@ -154,7 +156,7 @@ final class ChannelContext implements Context /** * @Given /^the (channel "[^"]+") has showing the lowest price of discounted products (enabled|disabled)$/ */ - public function theChannelHasShowingTheLowestPriceOfDiscountedProducts(ChannelInterface $channel, string $visible) + public function theChannelHasShowingTheLowestPriceOfDiscountedProducts(ChannelInterface $channel, string $visible): void { $channel->getChannelPriceHistoryConfig()->setLowestPriceForDiscountedProductsVisible($visible === 'enabled'); @@ -164,7 +166,7 @@ final class ChannelContext implements Context /** * @Given channel :channel has been deleted */ - public function iChannelHasBeenDeleted(ChannelInterface $channel) + public function iChannelHasBeenDeleted(ChannelInterface $channel): void { $this->channelRepository->remove($channel); } @@ -172,7 +174,7 @@ final class ChannelContext implements Context /** * @Given /^(its) default tax zone is (zone "([^"]+)")$/ */ - public function itsDefaultTaxRateIs(ChannelInterface $channel, ZoneInterface $defaultTaxZone) + public function itsDefaultTaxRateIs(ChannelInterface $channel, ZoneInterface $defaultTaxZone): void { $channel->setDefaultTaxZone($defaultTaxZone); $this->channelManager->flush(); @@ -182,7 +184,7 @@ final class ChannelContext implements Context * @Given /^(this channel) has contact email set as "([^"]+)"$/ * @Given /^(this channel) has no contact email set$/ */ - public function thisChannelHasContactEmailSetAs(ChannelInterface $channel, $contactEmail = null) + public function thisChannelHasContactEmailSetAs(ChannelInterface $channel, ?string $contactEmail = null): void { $channel->setContactEmail($contactEmail); $this->channelManager->flush(); @@ -191,7 +193,7 @@ final class ChannelContext implements Context /** * @Given /^on (this channel) shipping step is skipped if only a single shipping method is available$/ */ - public function onThisChannelShippingStepIsSkippedIfOnlyASingleShippingMethodIsAvailable(ChannelInterface $channel) + public function onThisChannelShippingStepIsSkippedIfOnlyASingleShippingMethodIsAvailable(ChannelInterface $channel): void { $channel->setSkippingShippingStepAllowed(true); @@ -203,7 +205,7 @@ final class ChannelContext implements Context */ public function onThisChannelPaymentStepIsSkippedIfOnlyASinglePaymentMethodIsAvailable( ChannelInterface $channel, - ) { + ): void { $channel->setSkippingPaymentStepAllowed(true); $this->channelManager->flush(); @@ -368,10 +370,7 @@ final class ChannelContext implements Context $this->channelManager->flush(); } - /** - * @param bool $state - */ - private function changeChannelState(ChannelInterface $channel, $state) + private function changeChannelState(ChannelInterface $channel, bool $state): void { $channel->setEnabled($state); $this->channelManager->flush(); diff --git a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml index 78bc7958dd..0d10e1bf57 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml @@ -58,7 +58,6 @@ - From c39ed1e70494b92415d3c57a7113ea3d4d9b2eff Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Thu, 14 Dec 2023 16:17:57 +0100 Subject: [PATCH 11/44] [Behat] Modify ResponseChecker methods signature --- src/Sylius/Behat/Client/ResponseChecker.php | 89 +++++++++++-------- .../Behat/Client/ResponseCheckerInterface.php | 14 ++- .../Admin/ManagingProductImagesContext.php | 28 +++--- .../Api/Admin/ManagingTaxonImagesContext.php | 10 +-- 4 files changed, 86 insertions(+), 55 deletions(-) diff --git a/src/Sylius/Behat/Client/ResponseChecker.php b/src/Sylius/Behat/Client/ResponseChecker.php index 5ddd6a5abc..b1588ec869 100644 --- a/src/Sylius/Behat/Client/ResponseChecker.php +++ b/src/Sylius/Behat/Client/ResponseChecker.php @@ -132,52 +132,48 @@ final class ResponseChecker implements ResponseCheckerInterface return false; } - public function hasValueInAnySubresourceObjectCollection( + public function hasValuesInAnySubresourceObjectCollection( Response $response, string $subResource, - string $key, - int|string $expectedValue, + array $expectedValues, ): bool { - foreach ($this->getResponseContentValue($response, $subResource) as $resource) { - if (!is_array($resource)) { - throw new \InvalidArgumentException(sprintf( - 'Expected subresource "%s" to be an array, got "%s"', - $subResource, - gettype($resource), - )); + $resourceCollection = $this->getResponseContentValue($response, $subResource); + + $this->assertIsArray($resourceCollection); + + foreach ($resourceCollection as $resource) { + $this->assertIsArray($resource); + + foreach ($expectedValues as $key => $expectedValue) { + if (!array_key_exists($key, $resource) || $resource[$key] !== $expectedValue) { + continue 2; + } } - if (!array_key_exists($key, $resource)) { - throw new \InvalidArgumentException(sprintf( - 'Expected subresource "%s" to have key "%s", has these keys instead: "%s"', - $subResource, - $key, - implode(', ', array_keys($resource)), - )); - } - - if ($resource[$key] === $expectedValue) { - return true; - } + return true; } return false; } - public function hasValueInSubresourceObject(Response $response, string $subResource, string $key, int|string $expectedValue): bool - { + public function hasValuesInSubresourceObject( + Response $response, + string $subResource, + array $expectedValues, + ): bool { $resource = $this->getResponseContentValue($response, $subResource); - if (array_key_exists($key, $resource)) { - return $resource[$key] === $expectedValue; + $this->assertIsArray($resource); + + $this->assertAllExpectedKeysArePresent($expectedValues, $resource); + + foreach ($expectedValues as $key => $expectedValue) { + if ($resource[$key] !== $expectedValue) { + return false; + } } - throw new \InvalidArgumentException(sprintf( - 'Expected subresource "%s" to have key "%s", has these keys instead: "%s"', - $subResource, - $key, - implode(', ', array_keys($resource)), - )); + return true; } /** @param string|array $value */ @@ -270,9 +266,10 @@ final class ResponseChecker implements ResponseCheckerInterface Assert::keyExists( $content, $key, - SprintfResponseEscaper::provideMessageWithEscapedResponseContent( - 'Expected \'' . $key . '\' not found.', - $response, + sprintf( + 'Expected to get: "%s" key in response, got keys: [%s]', + $key, + implode(', ', array_keys($content)), ), ); @@ -289,4 +286,26 @@ final class ResponseChecker implements ResponseCheckerInterface return true; } + + private function assertIsArray(mixed $resource): void + { + Assert::isArray($resource, sprintf('Expected to get an array, got "%s"', gettype($resource))); + } + + /** + * @param array $expectedValues + * @param array $resource + */ + private function assertAllExpectedKeysArePresent(array $expectedValues, array $resource): void + { + Assert::count( + array_diff_key($expectedValues, $resource), + 0, + sprintf( + 'Expected values array has keys: [%s], that are not present in the responses keys: [%s]', + implode(', ', array_keys(array_diff_key($expectedValues, $resource))), + implode(', ', array_keys($resource)), + ), + ); + } } diff --git a/src/Sylius/Behat/Client/ResponseCheckerInterface.php b/src/Sylius/Behat/Client/ResponseCheckerInterface.php index 5d4a3a1ef0..b775005011 100644 --- a/src/Sylius/Behat/Client/ResponseCheckerInterface.php +++ b/src/Sylius/Behat/Client/ResponseCheckerInterface.php @@ -51,9 +51,19 @@ interface ResponseCheckerInterface public function hasItemWithValue(Response $response, string $key, int|string $value): bool; - public function hasValueInAnySubresourceObjectCollection(Response $response, string $subResource, string $key, int|string $expectedValue): bool; + /** @param array $expectedValues */ + public function hasValuesInAnySubresourceObjectCollection( + Response $response, + string $subResource, + array $expectedValues, + ): bool; - public function hasValueInSubresourceObject(Response $response, string $subResource, string $key, int|string $expectedValue): bool; + /** @param array $expectedValues */ + public function hasValuesInSubresourceObject( + Response $response, + string $subResource, + array $expectedValues, + ): bool; public function hasItemOnPositionWithValue(Response $response, int $position, string $key, array|string $value): bool; diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php index 056651f779..31e10a4d10 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php @@ -131,12 +131,14 @@ final class ManagingProductImagesContext implements Context */ public function theProductShouldHaveAnImageWithType(ProductInterface $product, string $type): void { - Assert::true($this->responseChecker->hasValueInAnySubresourceObjectCollection( - $this->client->show(Resources::PRODUCTS, $product->getCode()), - 'images', - 'type', - $type, - )); + Assert::true( + $this->responseChecker->hasValuesInAnySubresourceObjectCollection( + $this->client->show(Resources::PRODUCTS, $product->getCode()), + 'images', + ['type' => $type], + ), + sprintf('Product %s does not have an image with %s type', $product->getName(), $type), + ); } /** @@ -162,12 +164,14 @@ final class ManagingProductImagesContext implements Context */ public function thisProductShouldNotHaveAnyImagesWithType(ProductInterface $product, string $type): void { - Assert::false($this->responseChecker->hasValueInAnySubresourceObjectCollection( - $this->client->show(Resources::PRODUCTS, $product->getCode()), - 'images', - 'type', - $type, - )); + Assert::false( + $this->responseChecker->hasValuesInAnySubresourceObjectCollection( + $this->client->show(Resources::PRODUCTS, $product->getCode()), + 'images', + ['type' => $type], + ), + sprintf('Product %s does not have an image with %s type', $product->getName(), $type), + ); } /** diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php index 9d0bc33612..14e025ff8c 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php @@ -124,11 +124,10 @@ final class ManagingTaxonImagesContext implements Context */ public function thisTaxonShouldHaveAnImageWithType(TaxonInterface $taxon, string $type): void { - Assert::true($this->responseChecker->hasValueInAnySubresourceObjectCollection( + Assert::true($this->responseChecker->hasValuesInAnySubresourceObjectCollection( $this->client->show(Resources::TAXONS, $taxon->getCode()), 'images', - 'type', - $type, + ['type' => $type], )); } @@ -138,11 +137,10 @@ final class ManagingTaxonImagesContext implements Context */ public function thisTaxonShouldNotHaveAnyImagesWithType(TaxonInterface $taxon, string $type): void { - Assert::false($this->responseChecker->hasValueInAnySubresourceObjectCollection( + Assert::false($this->responseChecker->hasValuesInAnySubresourceObjectCollection( $this->client->show(Resources::TAXONS, $taxon->getCode()), 'images', - 'type', - $type, + ['type' => $type], )); } From 46037d5521fd740f64db1be5cbdad63282ff9d8b Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Wed, 20 Dec 2023 10:26:29 +0100 Subject: [PATCH 12/44] Make automatic refactoring running every day --- .github/workflows/refactor.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/refactor.yaml b/.github/workflows/refactor.yaml index b54cd24e16..d01cd62e8d 100644 --- a/.github/workflows/refactor.yaml +++ b/.github/workflows/refactor.yaml @@ -3,7 +3,7 @@ name: Refactor on: schedule: - - cron: "0 2 * * MON" # Run at 2am every Monday + cron: "0 2 * * *" # Run every day at 2am workflow_dispatch: ~ jobs: From 6b98f181253116eb6fc242b429f23dab12379e2b Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Mon, 18 Dec 2023 03:12:43 +0100 Subject: [PATCH 13/44] [Scenarios] Improve statistics scenarios --- features/admin/dashboard.feature | 44 ------------ features/admin/dashboard_per_channel.feature | 50 -------------- features/admin/statistics.feature | 67 +++++++++++++++++++ features/admin/statistics_per_channel.feature | 62 +++++++++++++++++ 4 files changed, 129 insertions(+), 94 deletions(-) delete mode 100644 features/admin/dashboard.feature delete mode 100644 features/admin/dashboard_per_channel.feature create mode 100644 features/admin/statistics.feature create mode 100644 features/admin/statistics_per_channel.feature diff --git a/features/admin/dashboard.feature b/features/admin/dashboard.feature deleted file mode 100644 index a2ecc1bbdf..0000000000 --- a/features/admin/dashboard.feature +++ /dev/null @@ -1,44 +0,0 @@ -@admin_dashboard -Feature: Statistics dashboard in a single channel - In order to have an overview of my sales - As an Administrator - I want to see overall statistics on my admin dashboard - - Background: - Given the store operates on a single channel in "United States" - And the store ships everywhere for Free - And the store allows paying Offline - And the store has a product "Sylius T-Shirt" - And this product has "Red XL" variant priced at "$40.00" - And I am logged in as an administrator - - @ui - Scenario: Seeing basic statistics for entire store - Given 3 customers have fulfilled 4 orders placed for total of "$8,566.00" - And 2 more customers have paid 2 orders placed for total of "$459.00" - When I open administration dashboard - Then I should see 6 new orders - And I should see 5 new customers - And there should be total sales of "$9,025.00" - And the average order value should be "$1,504.17" - - @ui - Scenario: Statistics include only fulfilled orders that were not cancelled - Given 4 customers have fulfilled 4 orders placed for total of "$5,241.00" - And 2 more customers have placed 2 orders for total of "$459.00" - And 2 customers have added products to the cart for total of "$3,450.00" - And a single customer has placed an order for total of "$1,000.00" - But the customer cancelled this order - When I open administration dashboard - Then I should see 4 new orders - And I should see 9 new customers - And there should be total sales of "$5,241.00" - And the average order value should be "$1,310.25" - - @ui - Scenario: Seeing recent orders and customers - Given 2 customers have placed 3 orders for total of "$340.00" - And 2 customers have added products to the cart for total of "$424.00" - When I open administration dashboard - Then I should see 4 new customers in the list - And I should see 3 new orders in the list diff --git a/features/admin/dashboard_per_channel.feature b/features/admin/dashboard_per_channel.feature deleted file mode 100644 index 0a2c751aa7..0000000000 --- a/features/admin/dashboard_per_channel.feature +++ /dev/null @@ -1,50 +0,0 @@ -@admin_dashboard -Feature: Statistics dashboard per channel - In order to have an overview of my sales - As an Administrator - I want to see overall statistics on my admin dashboard in a specific channel - - Background: - Given the store operates on a channel named "Poland" - And there is product "Onion" available in this channel - And the store operates on another channel named "United States" - And there is product "Banana" available in that channel - And the store ships everywhere for Free - And the store allows paying Offline - And I am logged in as an administrator - - @ui - Scenario: Seeing basic statistics for the first channel by default - Given 3 customers have fulfilled 4 orders placed for total of "$8,566.00" mostly "Onion" product - And 2 more customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product - When I open administration dashboard - Then I should see 4 new orders - And I should see 5 new customers - And there should be total sales of "$8,566.00" - And the average order value should be "$2,141.50" - - @ui - Scenario: Changing channel in administration dashboard - Given 4 customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product - And 2 more customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product - And 2 more customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product - When I open administration dashboard - And I choose "United States" channel - Then I should see 2 new orders - And I should see 8 new customers - And there should be total sales of "$459.00" - And the average order value should be "$229.50" - - @ui - Scenario: Seeing recent orders in a specific channel - Given 3 customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product - And 2 more customers have placed 2 orders for total of "$459.00" mostly "Banana" product - When I open administration dashboard for "Poland" channel - Then I should see 4 new orders in the list - - @ui - Scenario: Seeing recent orders in a specific channel - Given 3 customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product - And 2 more customers have placed 2 orders for total of "$459.00" mostly "Banana" product - When I open administration dashboard for "United States" channel - Then I should see 2 new orders in the list diff --git a/features/admin/statistics.feature b/features/admin/statistics.feature new file mode 100644 index 0000000000..2e3aeb0cb4 --- /dev/null +++ b/features/admin/statistics.feature @@ -0,0 +1,67 @@ +@admin_dashboard +Feature: Statistics + In order to gain insight into my sales performance and customers activity + As an Administrator + I want to view comprehensive statistics + + Background: + Given the store operates on a single channel in "United States" + And the store ships everywhere for Free + And the store allows paying Offline + And the store has a product "Sylius T-Shirt" + And this product has "Red XL" variant priced at "$40.00" + And I am logged in as an administrator + + @ui @no-api + Scenario: Seeing statistics for the current year and default channel when expectations are not specified + Given it is "2022-12-31" now + And 2 new customers have fulfilled 2 orders placed for total of "$1,000.00" + And it is "2023-01-01" now + And 3 new customers have fulfilled 4 orders placed for total of "$2,000.21" + And it is "2023-02-01" now + And 2 more new customers have paid 2 orders placed for total of "$5,000.37" + When I view statistics + Then I should see 5 new customers + And I should see 6 new orders + And there should be total sales of "$7,000.58" + And the average order value should be "$1,166.76" + + @ui @javascript + Scenario: Seeing statistics for the previous year + Given it is "2022-01-01" now + And 3 new customers have fulfilled 2 orders placed for total of "$2,000.00" + And it is "2023-02-01" now + And 4 more new customers have paid 5 orders placed for total of "$5,000.37" + And 2 more new customers have paid 2 orders placed for total of "$5,000.37" + When I view statistics for "United States" channel and previous year split by month + Then I should see 3 new customers + And I should see 2 new orders + And there should be total sales of "$2,000.00" + And the average order value should be "$1,000.00" + + @ui @javascript + Scenario: Seeing statistics for the next year + Given it is "2022-01-01" now + And 3 new customers have fulfilled 2 orders placed for total of "$2,000.00" + And it is "2023-02-01" now + And 4 more new customers have paid 5 orders placed for total of "$5,000.37" + And 2 more new customers have paid 2 orders placed for total of "$5,000.37" + When I view statistics for "United States" channel and previous year split by month + And I view statistics for "United States" channel and next year split by month + Then I should see 6 new customers + And I should see 7 new orders + And there should be total sales of "$10,000.74" + And the average order value should be "$1,428.68" + + @ui @javascript + Scenario: Seeing statistics that include only fulfilled orders that were not cancelled + Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" + And 2 more new customers have placed 2 orders for total of "$459.00" + And 2 new customers have added products to the cart for total of "$3,450.00" + And a single customer has placed an order for total of "$1,000.00" + But the customer cancelled this order + When I view statistics for "United States" channel and current year split by month + Then I should see 4 new orders + And I should see 9 new customers + And there should be total sales of "$5,241.00" + And the average order value should be "$1,310.25" diff --git a/features/admin/statistics_per_channel.feature b/features/admin/statistics_per_channel.feature new file mode 100644 index 0000000000..6037fd297f --- /dev/null +++ b/features/admin/statistics_per_channel.feature @@ -0,0 +1,62 @@ +@admin_dashboard +Feature: Statistics for a specific channel + In order to gain insight into my sales performance and customers activity in a specific channel + As an Administrator + I want to view comprehensive statistics + + Background: + Given the store operates on a channel named "WEB-POLAND" + And there is product "Onion" available in this channel + And the store operates on another channel named "WEB-US" + And there is product "Banana" available in that channel + And the store ships everywhere for Free + And the store allows paying Offline + And I am logged in as an administrator + + @ui @no-api + Scenario: Seeing basic statistics for the first channel by default + Given 3 new customers have fulfilled 4 orders placed for total of "$8,566.00" mostly "Onion" product + And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product + When I view statistics + Then I should see 4 new orders + And I should see 5 new customers + And there should be total sales of "$8,566.00" + And the average order value should be "$2,141.50" + + @ui @javascript + Scenario: Switching to the channel with only fulfilled orders + Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product + And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product + And 2 more new customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product + When I view statistics for "WEB-POLAND" channel and current year split by month + And I choose "WEB-US" channel + Then I should see 2 new orders + And I should see 8 new customers + And there should be total sales of "$459.00" + And the average order value should be "$229.50" + + @ui @javascript + Scenario: Switching to the channel with both fulfilled and placed orders + Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product + And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product + And 2 more new customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product + When I view statistics for "WEB-US" channel + And I choose "WEB-POLAND" channel + Then I should see 4 new orders + And I should see 8 new customers + And there should be total sales of "$5,241.00" + And the average order value should be "$1,310.25" + + @ui + Scenario: Seeing recent orders in a specific channel + Given 3 new customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product + And 2 more new customers have placed 2 orders for total of "$459.00" mostly "Banana" product + When I view statistics for "WEB-POLAND" channel + Then I should see 4 new orders in the list + + @ui + Scenario: Seeing recent orders in a specific channel + Given 3 new customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product + And 2 more new customers have placed 2 orders for total of "$459.00" mostly "Banana" product + When I view statistics for "WEB-US" channel + Then I should see 2 new orders in the list From 76919bf0a0d3c2c54a848a7b3f6d8479bb371aa0 Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Mon, 18 Dec 2023 03:17:04 +0100 Subject: [PATCH 14/44] [Behat] Improve statistics UI implementation --- .../Behat/Context/Setup/OrderContext.php | 18 +++-- .../Context/Ui/Admin/DashboardContext.php | 70 ++++++++++++++----- src/Sylius/Behat/Page/Admin/DashboardPage.php | 58 +++++++++++++-- .../Page/Admin/DashboardPageInterface.php | 8 +++ .../config/services/contexts/setup.xml | 1 + src/Sylius/Behat/Resources/config/suites.yml | 2 +- .../admin/{dashboard.yml => dashboard.yaml} | 1 + 7 files changed, 131 insertions(+), 27 deletions(-) rename src/Sylius/Behat/Resources/config/suites/ui/admin/{dashboard.yml => dashboard.yaml} (94%) diff --git a/src/Sylius/Behat/Context/Setup/OrderContext.php b/src/Sylius/Behat/Context/Setup/OrderContext.php index 46517c7a15..79967e7f00 100644 --- a/src/Sylius/Behat/Context/Setup/OrderContext.php +++ b/src/Sylius/Behat/Context/Setup/OrderContext.php @@ -17,6 +17,7 @@ use Behat\Behat\Context\Context; use Doctrine\Persistence\ObjectManager; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Calendar\Provider\DateTimeProviderInterface; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPricingInterface; @@ -64,6 +65,7 @@ final class OrderContext implements Context private ProductVariantResolverInterface $variantResolver, private OrderItemQuantityModifierInterface $itemQuantityModifier, private ObjectManager $objectManager, + private DateTimeProviderInterface $dateTimeProvider, ) { } @@ -558,7 +560,7 @@ final class OrderContext implements Context } /** - * @Given /^(\d+) customers have added products to the cart for total of ("[^"]+")$/ + * @Given /^(\d+) new customers have added products to the cart for total of ("[^"]+")$/ */ public function customersHaveAddedProductsToTheCartForTotalOf(int $numberOfCustomers, int $total): void { @@ -589,7 +591,7 @@ final class OrderContext implements Context } /** - * @Given /^(\d+) (?:|more )customers have placed (\d+) orders for total of ("[^"]+")$/ + * @Given /^(\d+) (?:|more )new customers have placed (\d+) orders for total of ("[^"]+")$/ */ public function customersHavePlacedOrdersForTotalOf(int $numberOfCustomers, int $numberOfOrders, int $total): void { @@ -597,7 +599,7 @@ final class OrderContext implements Context } /** - * @Given /^(\d+) customers have fulfilled (\d+) orders placed for total of ("[^"]+")$/ + * @Given /^(\d+) new customers have fulfilled (\d+) orders placed for total of ("[^"]+")$/ */ public function customersHaveFulfilledOrdersPlacedForTotalOf( int $numberOfCustomers, @@ -608,7 +610,7 @@ final class OrderContext implements Context } /** - * @Given /^(\d+) (?:|more )customers have placed (\d+) orders for total of ("[^"]+") mostly ("[^"]+" product)$/ + * @Given /^(\d+) (?:|more )new customers have placed (\d+) orders for total of ("[^"]+") mostly ("[^"]+" product)$/ */ public function customersHavePlacedOrdersForTotalOfMostlyProduct( int $numberOfCustomers, @@ -620,7 +622,7 @@ final class OrderContext implements Context } /** - * @Given /^(\d+) (?:|more )customers have fulfilled (\d+) orders placed for total of ("[^"]+") mostly ("[^"]+" product)$/ + * @Given /^(\d+) (?:|more )new customers have fulfilled (\d+) orders placed for total of ("[^"]+") mostly ("[^"]+" product)$/ */ public function customersHaveFulfilledOrdersPlacedForTotalOfMostlyProduct( int $numberOfCustomers, @@ -632,7 +634,7 @@ final class OrderContext implements Context } /** - * @Given /^(\d+) (?:|more )customers have paid (\d+) orders placed for total of ("[^"]+")$/ + * @Given /^(\d+) (?:|more )new customers have paid (\d+) orders placed for total of ("[^"]+")$/ */ public function moreCustomersHavePaidOrdersPlacedForTotalOf( int $numberOfCustomers, @@ -918,6 +920,8 @@ final class OrderContext implements Context $customer->setFirstname('John'); $customer->setLastname('Doe' . $i); + $customer->setCreatedAt($this->dateTimeProvider->now()); + $customers[] = $customer; $this->customerRepository->add($customer); @@ -1020,6 +1024,8 @@ final class OrderContext implements Context $this->shipOrder($order); } + $order->setCheckoutCompletedAt($this->dateTimeProvider->now()); + $this->objectManager->persist($order); $this->sharedStorage->set('order', $order); } diff --git a/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php b/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php index f99fd7f559..32b6209161 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php @@ -28,8 +28,9 @@ final class DashboardContext implements Context /** * @Given I am on the administration dashboard * @When I (try to )open administration dashboard + * @When I (try to )view statistics */ - public function iOpenAdministrationDashboard(): void + public function iViewStatistics(): void { try { $this->dashboardPage->open(); @@ -38,17 +39,50 @@ final class DashboardContext implements Context } /** - * @When I open administration dashboard for :name channel + * @When I view statistics for :name channel + * + * @throws UnexpectedPageException */ - public function iOpenAdministrationDashboardForChannel($name) + public function iViewStatisticsForChannel(string $channelName): void { - $this->dashboardPage->open(['channel' => StringInflector::nameToLowercaseCode($name)]); + $channelName === 'United States' ? + $channelName = 'WEB-US' : $channelName = StringInflector::nameToLowercaseCode($channelName); + + $this->dashboardPage->open(['channel' => $channelName]); + } + + /** + * @When /^I view statistics for "([^"]+)" channel and (current|previous|next) year split by (month|day)$/ + * + * @throws UnexpectedPageException + */ + public function iViewStatisticsForChannelAndYear( + string $channelName, + string $period, + string $interval, + ): void { + $channelName === 'United States' + ? $channelName = 'WEB-US' : $channelName = StringInflector::nameToLowercaseCode($channelName); + + $this->dashboardPage->open(['channel' => $channelName]); + + match ($interval) { + 'month' => $this->dashboardPage->chooseYearSplitByMonthsInterval(), + 'day' => $this->dashboardPage->chooseMonthSplitByDaysInterval(), + default => throw new \InvalidArgumentException(sprintf('Interval "%s" is not supported.', $interval)), + }; + + match ($period) { + 'previous' => $this->dashboardPage->choosePreviousPeriod(), + 'next' => $this->dashboardPage->chooseNextPeriod(), + default => null, + }; } /** * @When I choose :channelName channel */ - public function iChooseChannel($channelName) + public function iChooseChannel(string $channelName): void { $this->dashboardPage->chooseChannel($channelName); } @@ -64,23 +98,23 @@ final class DashboardContext implements Context /** * @Then I should see :number new orders */ - public function iShouldSeeNewOrders($number) + public function iShouldSeeNewOrders(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewOrders(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewOrders(), $number); } /** * @Then I should see :number new customers */ - public function iShouldSeeNewCustomers($number) + public function iShouldSeeNewCustomers(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewCustomers(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewCustomers(), $number); } /** * @Then there should be total sales of :total */ - public function thereShouldBeTotalSalesOf($total) + public function thereShouldBeTotalSalesOf(string $total): void { Assert::same($this->dashboardPage->getTotalSales(), $total); } @@ -88,25 +122,29 @@ final class DashboardContext implements Context /** * @Then the average order value should be :value */ - public function myAverageOrderValueShouldBe($value) + public function myAverageOrderValueShouldBe(string $value): void { - Assert::same($this->dashboardPage->getAverageOrderValue(), $value); + Assert::same( + $this->dashboardPage->getAverageOrderValue(), + $value, + 'Expected average order value to be equal to %2$s, but it is %s.', + ); } /** * @Then I should see :number new customers in the list */ - public function iShouldSeeNewCustomersInTheList($number) + public function iShouldSeeNewCustomersInTheList(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewCustomersInTheList(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewCustomersInTheList(), $number); } /** * @Then I should see :number new orders in the list */ - public function iShouldSeeNewOrdersInTheList($number) + public function iShouldSeeNewOrdersInTheList(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewOrdersInTheList(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewOrdersInTheList(), $number); } /** diff --git a/src/Sylius/Behat/Page/Admin/DashboardPage.php b/src/Sylius/Behat/Page/Admin/DashboardPage.php index b98e4c5b5b..c639bf66be 100644 --- a/src/Sylius/Behat/Page/Admin/DashboardPage.php +++ b/src/Sylius/Behat/Page/Admin/DashboardPage.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Page\Admin; +use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Session; use FriendsOfBehat\PageObjectExtension\Page\SymfonyPage; use Sylius\Behat\Service\Accessor\TableAccessorInterface; @@ -20,84 +21,133 @@ use Symfony\Component\Routing\RouterInterface; class DashboardPage extends SymfonyPage implements DashboardPageInterface { + /** + * @template TKey of array-key + * @template TValue + * + * @param array|\ArrayAccess $minkParameters + */ public function __construct( Session $session, - $minkParameters, + array|\ArrayAccess $minkParameters, RouterInterface $router, protected TableAccessorInterface $tableAccessor, ) { parent::__construct($session, $minkParameters, $router); } + /** @throws ElementNotFoundException */ public function getTotalSales(): string { return $this->getElement('total_sales')->getText(); } + /** @throws ElementNotFoundException */ public function getNumberOfNewOrders(): int { return (int) $this->getElement('new_orders')->getText(); } + /** @throws ElementNotFoundException */ public function getNumberOfNewOrdersInTheList(): int { return $this->tableAccessor->countTableBodyRows($this->getElement('order_list')); } + /** @throws ElementNotFoundException */ public function getNumberOfNewCustomers(): int { return (int) $this->getElement('new_customers')->getText(); } + /** @throws ElementNotFoundException */ public function getNumberOfNewCustomersInTheList(): int { return $this->tableAccessor->countTableBodyRows($this->getElement('customer_list')); } + /** @throws ElementNotFoundException */ public function getAverageOrderValue(): string { return $this->getElement('average_order_value')->getText(); } + /** @throws ElementNotFoundException */ public function getSubHeader(): string { return trim($this->getElement('sub_header')->getText()); } + /** @throws ElementNotFoundException */ public function isSectionWithLabelVisible(string $name): bool { return $this->getElement('admin_menu')->find('css', sprintf('div:contains(%s)', $name)) !== null; } + /** @throws ElementNotFoundException */ public function logOut(): void { $this->getElement('logout')->click(); } + /** @throws ElementNotFoundException */ public function chooseChannel(string $channelName): void { $this->getElement('channel_choosing_link', ['%channelName%' => $channelName])->click(); } + /** @throws ElementNotFoundException */ + public function chooseYearSplitByMonthsInterval(): void + { + $this->getElement('year_split_by_months_statistics_button')->click(); + } + + /** @throws ElementNotFoundException */ + public function chooseMonthSplitByDaysInterval(): void + { + $this->getElement('month_split_by_days_statistics_button')->click(); + } + + /** @throws ElementNotFoundException */ + public function choosePreviousPeriod(): void + { + $this->getElement('navigation_previous')->click(); + + usleep(500000); + } + + /** @throws ElementNotFoundException */ + public function chooseNextPeriod(): void + { + $this->getElement('navigation_next')->click(); + + usleep(500000); + } + public function getRouteName(): string { return 'sylius_admin_dashboard'; } + /** @return array */ protected function getDefinedElements(): array { return array_merge(parent::getDefinedElements(), [ + 'admin_menu' => '.sylius-admin-menu', 'average_order_value' => '#average-order-value', + 'channel_choosing_link' => 'a:contains("%channelName%")', 'customer_list' => '#customers', 'dropdown' => 'i.dropdown', 'logout' => '#sylius-logout-button', + 'month_split_by_days_statistics_button' => 'button[data-stats-button="month"]', + 'navigation_next' => '#navigation-next', + 'navigation_previous' => '#navigation-prev', 'new_customers' => '#new-customers', 'new_orders' => '#new-orders', 'order_list' => '#orders', - 'total_sales' => '#total-sales', 'sub_header' => '.ui.header .content .sub.header', - 'channel_choosing_link' => 'a:contains("%channelName%")', - 'admin_menu' => '.sylius-admin-menu', + 'total_sales' => '#total-sales', + 'year_split_by_months_statistics_button' => 'button[data-stats-button="year"]', ]); } } diff --git a/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php b/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php index 2172b62edc..24cd7f0156 100644 --- a/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php @@ -36,4 +36,12 @@ interface DashboardPageInterface extends SymfonyPageInterface public function logOut(): void; public function chooseChannel(string $channelName): void; + + public function chooseYearSplitByMonthsInterval(): void; + + public function chooseMonthSplitByDaysInterval(): void; + + public function choosePreviousPeriod(): void; + + public function chooseNextPeriod(): void; } diff --git a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml index 0d10e1bf57..b06f40e1f4 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml @@ -123,6 +123,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/suites.yml b/src/Sylius/Behat/Resources/config/suites.yml index b4d6f06516..a45b92fb0e 100644 --- a/src/Sylius/Behat/Resources/config/suites.yml +++ b/src/Sylius/Behat/Resources/config/suites.yml @@ -90,7 +90,7 @@ imports: - suites/ui/account/login.yml - suites/ui/addressing/managing_countries.yml - suites/ui/addressing/managing_zones.yml - - suites/ui/admin/dashboard.yml + - suites/ui/admin/dashboard.yaml - suites/ui/admin/impersonating_customers.yml - suites/ui/admin/locale.yml - suites/ui/admin/login.yml diff --git a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yml b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml similarity index 94% rename from src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yml rename to src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml index d9db8650f1..e6900bcfdf 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml @@ -18,6 +18,7 @@ default: - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.shipping - sylius.behat.context.setup.zone + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.transform.customer - sylius.behat.context.transform.lexical From 72d559127f4e3a832633f7101d099378f359e7b0 Mon Sep 17 00:00:00 2001 From: Rafikooo Date: Wed, 20 Dec 2023 11:02:20 +0100 Subject: [PATCH 15/44] [Behat] Use ChannelTransformer in DashboardContext --- .../Context/Transform/ChannelContext.php | 10 ++++++++-- .../Context/Ui/Admin/DashboardContext.php | 20 +++++++------------ .../config/suites/ui/admin/dashboard.yaml | 1 + 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Sylius/Behat/Context/Transform/ChannelContext.php b/src/Sylius/Behat/Context/Transform/ChannelContext.php index 92a7cb38d5..cc1edad1b5 100644 --- a/src/Sylius/Behat/Context/Transform/ChannelContext.php +++ b/src/Sylius/Behat/Context/Transform/ChannelContext.php @@ -15,10 +15,14 @@ namespace Sylius\Behat\Context\Transform; use Behat\Behat\Context\Context; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Webmozart\Assert\Assert; final class ChannelContext implements Context { + /** + * @param ChannelRepositoryInterface $channelRepository + */ public function __construct(private ChannelRepositoryInterface $channelRepository) { } @@ -29,7 +33,7 @@ final class ChannelContext implements Context * @Transform /^channel to "([^"]+)"$/ * @Transform :channel */ - public function getChannelByName($channelName) + public function getChannelByName(string $channelName) { $channels = $this->channelRepository->findByName($channelName); @@ -44,8 +48,10 @@ final class ChannelContext implements Context /** * @Transform all channels + * + * @return array */ - public function getAllChannels() + public function getAllChannels(): array { return $this->channelRepository->findAll(); } diff --git a/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php b/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php index 32b6209161..9a578dd28b 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php @@ -16,7 +16,7 @@ namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; use FriendsOfBehat\PageObjectExtension\Page\UnexpectedPageException; use Sylius\Behat\Page\Admin\DashboardPageInterface; -use Sylius\Component\Core\Formatter\StringInflector; +use Sylius\Component\Core\Model\ChannelInterface; use Webmozart\Assert\Assert; final class DashboardContext implements Context @@ -39,32 +39,26 @@ final class DashboardContext implements Context } /** - * @When I view statistics for :name channel + * @When I view statistics for :channel channel * * @throws UnexpectedPageException */ - public function iViewStatisticsForChannel(string $channelName): void + public function iViewStatisticsForChannel(ChannelInterface $channel): void { - $channelName === 'United States' ? - $channelName = 'WEB-US' : $channelName = StringInflector::nameToLowercaseCode($channelName); - - $this->dashboardPage->open(['channel' => $channelName]); + $this->dashboardPage->open(['channel' => $channel->getCode()]); } /** - * @When /^I view statistics for "([^"]+)" channel and (current|previous|next) year split by (month|day)$/ + * @When /^I view statistics for ("[^"]+" channel) and (current|previous|next) year split by (month|day)$/ * * @throws UnexpectedPageException */ public function iViewStatisticsForChannelAndYear( - string $channelName, + ChannelInterface $channel, string $period, string $interval, ): void { - $channelName === 'United States' - ? $channelName = 'WEB-US' : $channelName = StringInflector::nameToLowercaseCode($channelName); - - $this->dashboardPage->open(['channel' => $channelName]); + $this->dashboardPage->open(['channel' => $channel->getCode()]); match ($interval) { 'month' => $this->dashboardPage->chooseYearSplitByMonthsInterval(), diff --git a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml index e6900bcfdf..e83a16d9a4 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml +++ b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml @@ -20,6 +20,7 @@ default: - sylius.behat.context.setup.zone - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext + - sylius.behat.context.transform.channel - sylius.behat.context.transform.customer - sylius.behat.context.transform.lexical - sylius.behat.context.transform.order From 0072784228b436e4cfe133daabac1ef31268295f Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Wed, 20 Dec 2023 10:43:50 +0100 Subject: [PATCH 16/44] [ApiBundle][Address] Add address log entries operation --- ...racking_changes_on_order_addresses.feature | 14 +-- .../Api/Admin/ManagingOrdersContext.php | 41 +++++++- src/Sylius/Behat/Context/Api/Subresources.php | 2 + .../Ui/Admin/ManagingOrdersContext.php | 14 ++- .../Behat/Page/Admin/Order/HistoryPage.php | 5 + .../Page/Admin/Order/HistoryPageInterface.php | 2 + .../config/services/contexts/api/admin.xml | 3 +- src/Sylius/Behat/Resources/config/suites.yml | 1 + .../order/modifying_placed_order_address.yaml | 2 +- .../suites/api/order/order_history.yaml | 37 +++++++ .../Controller/GetAddressLogEntryAction.php | 41 ++++++++ .../AddressLogEntryDocumentationModifier.php | 96 +++++++++++++++++++ .../ApiBundle/Query/GetAddressLogEntry.php | 28 ++++++ .../GetAddressLogEntryHandler.php | 37 +++++++ .../config/api_resources/Address.xml | 13 +++ .../Resources/config/integrations/swagger.xml | 4 + .../config/serialization/AddressLogEntry.xml | 32 +++++++ .../Resources/config/services/controllers.xml | 6 ++ .../config/services/query_handlers.xml | 4 + .../GetAddressLogEntryHandlerSpec.php | 43 +++++++++ tests/Api/Admin/AddressesTest.php | 96 +++++++++++++++++++ .../Expected/admin/address/get_address.json | 17 ++++ .../address/get_address_log_entries.json | 26 +++++ .../admin/address/update_address.json | 17 ++++ 24 files changed, 569 insertions(+), 12 deletions(-) create mode 100644 src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml create mode 100644 src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php create mode 100644 src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php create mode 100644 src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php create mode 100644 src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php create mode 100644 src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml create mode 100644 src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php create mode 100644 tests/Api/Admin/AddressesTest.php create mode 100644 tests/Api/Responses/Expected/admin/address/get_address.json create mode 100644 tests/Api/Responses/Expected/admin/address/get_address_log_entries.json create mode 100644 tests/Api/Responses/Expected/admin/address/update_address.json diff --git a/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature b/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature index 68138af07e..c1fe15488a 100644 --- a/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature +++ b/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature @@ -13,20 +13,22 @@ Feature: Tracking changes on order addresses And the customer bought a single "Italian suit" When I am logged in as an administrator - @ui + @api @ui Scenario: Browsing order's addresses history after changing it by customer Given the customer "Barney Stinson" addressed it to "East 84st Street and 3rd Avenue", "10118" "New York" in the "United States" with identical billing address And the customer changed shipping address' street to "211 Madison Avenue" And the customer chose "Free" shipping method with "Cash on Delivery" payment When I browse order's "#00000001" history - Then there should be 2 changes in the registry + Then there should be 2 shipping address changes in the registry + Then there should be 1 billing address changes in the registry - @ui + @api @ui Scenario: Browsing order's addresses history after changing it by administrator Given the customer "Barney Stinson" addressed it to "East 84st Street and 3rd Avenue", "10118" "New York" in the "United States" with identical billing address And the customer chose "Free" shipping method with "Cash on Delivery" payment - When I want to modify a customer's shipping address of this order - And I specify their shipping address as "New York", "150 W. 85th Street", "10028", "United States" for "Ted Mosby" + When I want to modify a customer's billing address of this order + And I specify their billing address as "New York", "150 W. 85th Street", "10028", "United States" for "Ted Mosby" And I save my changes And I browse order's "#00000001" history - Then there should be 2 changes in the registry + Then there should be 1 shipping address changes in the registry + Then there should be 2 billing address changes in the registry diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php index 0f5ae24a58..92bf3eacd9 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php @@ -18,6 +18,8 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Context\Api\Subresources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Behat\Service\SecurityServiceInterface; use Sylius\Behat\Service\SharedSecurityServiceInterface; use Sylius\Behat\Service\SharedStorageInterface; @@ -45,6 +47,7 @@ final class ManagingOrdersContext implements Context private SecurityServiceInterface $adminSecurityService, private SharedStorageInterface $sharedStorage, private SharedSecurityServiceInterface $sharedSecurityService, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, ) { } @@ -55,7 +58,7 @@ final class ManagingOrdersContext implements Context public function iSeeTheOrder(OrderInterface $order): void { $response = $this->client->show(Resources::ORDERS, $order->getTokenValue()); - Assert::same($this->responseChecker->getValue($response, '@id'), $this->iriConverter->getIriFromResource($order)); + Assert::same($this->responseChecker->getValue($response, '@id'), $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin')); $this->sharedStorage->set('order', $order); } @@ -69,6 +72,14 @@ final class ManagingOrdersContext implements Context $this->client->index(Resources::ORDERS); } + /** + * @When I browse order's :order history + */ + public function iBrowseOrderHistory(OrderInterface $order): void + { + $this->iSeeTheOrder($order); + } + /** * @When I filter */ @@ -978,6 +989,34 @@ final class ManagingOrdersContext implements Context Assert::same($this->getTotalAsInt($subTotal), $orderItem['unitPrice'] * $orderItem['quantity'] + $unitPromotionAdjustments); } + /** + * @Then there should be :count shipping address changes in the registry + */ + public function thereShouldBeCountShippingAddressChangesInTheRegistry(int $count): void + { + $order = $this->sharedStorage->get('order'); + $response = $this->client->subResourceIndex( + Resources::ADDRESSES, + Subresources::ADDRESSES_LOG_ENTRIES, + (string) $order->getShippingAddress()->getId(), + ); + Assert::same($this->responseChecker->countCollectionItems($response), $count); + } + + /** + * @Then there should be :count billing address changes in the registry + */ + public function thereShouldBeCountBillingAddressChangesInTheRegistry(int $count): void + { + $order = $this->sharedStorage->get('order'); + $response = $this->client->subResourceIndex( + Resources::ADDRESSES, + Subresources::ADDRESSES_LOG_ENTRIES, + (string) $order->getBillingAddress()->getId(), + ); + Assert::same($this->responseChecker->countCollectionItems($response), $count); + } + /** * @param array $address */ diff --git a/src/Sylius/Behat/Context/Api/Subresources.php b/src/Sylius/Behat/Context/Api/Subresources.php index 58318f32d3..7c6c0743b5 100644 --- a/src/Sylius/Behat/Context/Api/Subresources.php +++ b/src/Sylius/Behat/Context/Api/Subresources.php @@ -17,6 +17,8 @@ final class Subresources { public const PROMOTION_COUPONS = 'coupons'; + public const ADDRESSES_LOG_ENTRIES = 'log-entries'; + private function __construct() { } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php index 147e879e3f..78ba74a854 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php @@ -888,11 +888,19 @@ final class ManagingOrdersContext implements Context } /** - * @Then there should be :count changes in the registry + * @Then there should be :count shipping address changes in the registry */ - public function thereShouldBeCountChangesInTheRegistry($count) + public function thereShouldBeCountShippingAddressChangesInTheRegistry(int $count): void { - Assert::same($this->historyPage->countShippingAddressChanges(), (int) $count); + Assert::same($this->historyPage->countShippingAddressChanges(), $count); + } + + /** + * @Then there should be :count billing address changes in the registry + */ + public function thereShouldBeCountBillingAddressChangesInTheRegistry(int $count): void + { + Assert::same($this->historyPage->countBillingAddressChanges(), $count); } /** diff --git a/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php b/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php index 1b54fa6c53..a694a6cf3f 100644 --- a/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php +++ b/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php @@ -26,4 +26,9 @@ class HistoryPage extends SymfonyPage implements HistoryPageInterface { return count($this->getDocument()->findAll('css', '#shipping-address-changes tbody tr')); } + + public function countBillingAddressChanges(): int + { + return count($this->getDocument()->findAll('css', '#billing-address-changes tbody tr')); + } } diff --git a/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php b/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php index 990077d5f6..10d174abab 100644 --- a/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php @@ -18,4 +18,6 @@ use FriendsOfBehat\PageObjectExtension\Page\SymfonyPageInterface; interface HistoryPageInterface extends SymfonyPageInterface { public function countShippingAddressChanges(): int; + + public function countBillingAddressChanges(): int; } diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index 6ef0d9482b..1154574f15 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -214,6 +214,7 @@ + @@ -291,7 +292,7 @@ - + diff --git a/src/Sylius/Behat/Resources/config/suites.yml b/src/Sylius/Behat/Resources/config/suites.yml index a45b92fb0e..2173fa4722 100644 --- a/src/Sylius/Behat/Resources/config/suites.yml +++ b/src/Sylius/Behat/Resources/config/suites.yml @@ -30,6 +30,7 @@ imports: - suites/api/locale/managing_locales.yml - suites/api/order/managing_orders.yml - suites/api/order/modifying_placed_order_address.yaml + - suites/api/order/order_history.yaml - suites/api/payment/managing_payment_methods.yaml - suites/api/payment/managing_payments.yml - suites/api/product/adding_product_review.yml diff --git a/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml b/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml index d1db06d939..2f1f158a6d 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml +++ b/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml @@ -36,9 +36,9 @@ default: - sylius.behat.context.transform.shared_storage - sylius.behat.context.api.admin.managing_orders + - sylius.behat.context.api.admin.managing_placed_order_addresses - sylius.behat.context.api.admin.response - sylius.behat.context.api.admin.save - - Sylius\Behat\Context\Api\Admin\ManagingPlacedOrderAddressesContext filters: tags: "@modifying_placed_order_address&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml b/src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml new file mode 100644 index 0000000000..556369a06e --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml @@ -0,0 +1,37 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_order_history: + contexts: + - sylius.behat.context.hook.doctrine_orm + - sylius.behat.context.hook.session + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.shipping + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.country + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.order + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.zone + + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.api.admin.managing_orders + - sylius.behat.context.api.admin.managing_placed_order_addresses + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@order_history&&@api" diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php new file mode 100644 index 0000000000..f641b04fdf --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php @@ -0,0 +1,41 @@ +messageBus = $messageBus; + } + + /** @return Collection */ + public function __invoke(int $id): Collection + { + return $this->handle( + $this->messageBus->dispatch( + new GetAddressLogEntry($id), + ), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php new file mode 100644 index 0000000000..75eb956af2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php @@ -0,0 +1,96 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['Address-admin.address.log_entry.read'] = [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'action' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'version' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'data' => [ + 'readOnly' => true, + 'type' => 'object', + ], + 'logged_at' => [ + 'readOnly' => true, + 'type' => 'string', + 'format' => 'date-time', + ], + ], + ], + ]; + + $schemas['Address.jsonld-admin.address.log_entry.read'] = [ + 'type' => 'object', + 'properties' => [ + 'hydra:member' => [ + 'readOnly' => true, + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + '@type' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'action' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'version' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'data' => [ + 'readOnly' => true, + 'type' => 'object', + ], + 'logged_at' => [ + 'readOnly' => true, + 'type' => 'string', + 'format' => 'date-time', + ], + ], + ], + ], + 'hydra:totalItems' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + ], + ]; + + $components = $components->withSchemas($schemas); + + return $docs->withComponents($components); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php b/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php new file mode 100644 index 0000000000..794aad9051 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php @@ -0,0 +1,28 @@ +addressId; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php new file mode 100644 index 0000000000..dfbffffb0f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php @@ -0,0 +1,37 @@ + */ + public function __invoke(GetAddressLogEntry $query): Collection + { + $queryBuilder = $this->addressLogEntryRepository->createByObjectIdQueryBuilder((string)$query->getAddressId()); + + return new ArrayCollection($queryBuilder->getQuery()->getResult()); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml index 2c64a38a87..a4b27133bd 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml @@ -61,6 +61,19 @@ + + GET + /admin/addresses/{id}/log-entries + Sylius\Bundle\ApiBundle\Controller\GetAddressLogEntryAction + + admin:address:log_entry:read + + + Retrieves the collection of AddressLogEntry resources. + Retrieves the collection of AddressLogEntry resources. + + + GET /shop/addresses/{id} diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml index a7c1cc9202..d7a6eed471 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml @@ -92,5 +92,9 @@ %sylius.model.adjustment.class% + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml new file mode 100644 index 0000000000..6aad46bc99 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml @@ -0,0 +1,32 @@ + + + + + + + + admin:address:log_entry:read + + + admin:address:log_entry:read + + + admin:address:log_entry:read + + + admin:address:log_entry:read + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml index f8eca679f3..fde69c9dab 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml @@ -54,5 +54,11 @@ + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml index 7c8e2be41c..aa75caeb6f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml @@ -24,5 +24,9 @@ + + + + diff --git a/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php new file mode 100644 index 0000000000..ac4632bde5 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php @@ -0,0 +1,43 @@ +beConstructedWith($addressLogEntryRepository); + } + + function it_returns_address_log_entries_for_a_given_customer( + ResourceLogEntryRepositoryInterface $addressLogEntryRepository, + QueryBuilder $queryBuilder, + AbstractQuery $query, + LogEntryInterface $addressLogEntryOne, + LogEntryInterface $addressLogEntryTwo, + ): void { + $query->getResult()->willReturn([$addressLogEntryOne, $addressLogEntryTwo]); + $queryBuilder->getQuery()->willReturn($query); + $addressLogEntryRepository->createByObjectIdQueryBuilder('3')->willReturn($queryBuilder); + + $this(new GetAddressLogEntry(3))->shouldIterateAs([$addressLogEntryOne, $addressLogEntryTwo]); + } +} diff --git a/tests/Api/Admin/AddressesTest.php b/tests/Api/Admin/AddressesTest.php new file mode 100644 index 0000000000..9e3e9caf60 --- /dev/null +++ b/tests/Api/Admin/AddressesTest.php @@ -0,0 +1,96 @@ +loadFixturesFromFiles(['authentication/api_administrator.yaml', 'address_with_customer.yaml']); + + /** @var AddressInterface $address */ + $address = $fixtures['address']; + + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/addresses/%d', $address->getId()), + server: $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/address/get_address', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_updates_address(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'address_with_customer.yaml']); + + /** @var AddressInterface $address */ + $address = $fixtures['address']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/addresses/%d', $address->getId()), + server: $this->headerBuilder()->withJsonLdContentType()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(), + content: json_encode([ + 'firstName' => 'Finley', + 'lastName' => 'Ward', + 'company' => 'Company', + 'countryCode' => 'UK', + 'street' => 'New Henry St', + 'city' => 'Neath', + 'postcode' => 'SA11 1PH', + 'phoneNumber' => '01639 644902', + 'provinceName' => 'West Glamorgan', + 'provinceCode' => 'WGM', + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/address/update_address', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_gets_address_log_entries(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'address_with_customer.yaml']); + + /** @var AddressInterface $address */ + $address = $fixtures['address']; + + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/addresses/%d/log-entries', $address->getId()), + server: $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/address/get_address_log_entries', + Response::HTTP_OK, + ); + } +} diff --git a/tests/Api/Responses/Expected/admin/address/get_address.json b/tests/Api/Responses/Expected/admin/address/get_address.json new file mode 100644 index 0000000000..be6c5ecfa7 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/address/get_address.json @@ -0,0 +1,17 @@ +{ + "@context": "\/api\/v2\/contexts\/Address", + "@id": "\/api\/v2\/admin\/addresses\/@integer@", + "@type": "Address", + "customer": "\/api\/v2\/admin\/customers\/@integer@", + "id": @integer@, + "firstName": "John", + "lastName": "Doe", + "phoneNumber": "123456789", + "company": "CocaCola", + "countryCode": "US", + "provinceCode": "999", + "provinceName": "east", + "street": "Green Avenue", + "city": "New York", + "postcode": "00000" +} diff --git a/tests/Api/Responses/Expected/admin/address/get_address_log_entries.json b/tests/Api/Responses/Expected/admin/address/get_address_log_entries.json new file mode 100644 index 0000000000..bc2be3d2d5 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/address/get_address_log_entries.json @@ -0,0 +1,26 @@ +{ + "@context": "\/api\/v2\/contexts\/Address", + "@id": "\/api\/v2\/shop\/addresses", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@type": "AddressLogEntry", + "action": "create", + "version": 1, + "data": { + "firstName": "John", + "lastName": "Doe", + "phoneNumber": "123456789", + "street": "Green Avenue", + "company": "CocaCola", + "city": "New York", + "postcode": "00000", + "countryCode": "US", + "provinceCode": "999", + "provinceName": "east" + }, + "logged_at": @date@ + } + ], + "hydra:totalItems": 1 +} diff --git a/tests/Api/Responses/Expected/admin/address/update_address.json b/tests/Api/Responses/Expected/admin/address/update_address.json new file mode 100644 index 0000000000..b812af90c9 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/address/update_address.json @@ -0,0 +1,17 @@ +{ + "@context": "\/api\/v2\/contexts\/Address", + "@id": "\/api\/v2\/admin\/addresses\/@integer@", + "@type": "Address", + "customer": "\/api\/v2\/admin\/customers\/@integer@", + "id": @integer@, + "firstName": "Finley", + "lastName": "Ward", + "phoneNumber": "01639 644902", + "company": "Company", + "countryCode": "UK", + "provinceCode": "WGM", + "provinceName": "West Glamorgan", + "street": "New Henry St", + "city": "Neath", + "postcode": "SA11 1PH" +} From ceba4a89422e1ed0cb2e6f4d6a067e09e9a250cb Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Wed, 20 Dec 2023 12:08:30 +0100 Subject: [PATCH 17/44] [ApiBundle][Address] Rename GetAddressLogEntryAction to GetAddressLogEntryCollectionAction --- ...tryAction.php => GetAddressLogEntryCollectionAction.php} | 6 +++--- ...AddressLogEntry.php => GetAddressLogEntryCollection.php} | 2 +- ...yHandler.php => GetAddressLogEntryCollectionHandler.php} | 6 +++--- .../ApiBundle/Resources/config/api_resources/Address.xml | 2 +- .../ApiBundle/Resources/config/services/controllers.xml | 2 +- .../ApiBundle/Resources/config/services/query_handlers.xml | 2 +- ...Spec.php => GetAddressLogEntryCollectionHandlerSpec.php} | 6 +++--- 7 files changed, 13 insertions(+), 13 deletions(-) rename src/Sylius/Bundle/ApiBundle/Controller/{GetAddressLogEntryAction.php => GetAddressLogEntryCollectionAction.php} (83%) rename src/Sylius/Bundle/ApiBundle/Query/{GetAddressLogEntry.php => GetAddressLogEntryCollection.php} (92%) rename src/Sylius/Bundle/ApiBundle/QueryHandler/{GetAddressLogEntryHandler.php => GetAddressLogEntryCollectionHandler.php} (83%) rename src/Sylius/Bundle/ApiBundle/spec/QueryHandler/{GetAddressLogEntryHandlerSpec.php => GetAddressLogEntryCollectionHandlerSpec.php} (83%) diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryCollectionAction.php similarity index 83% rename from src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php rename to src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryCollectionAction.php index f641b04fdf..23ee49e28e 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryAction.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryCollectionAction.php @@ -14,13 +14,13 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Controller; use Doctrine\Common\Collections\Collection; -use Sylius\Bundle\ApiBundle\Query\GetAddressLogEntry; +use Sylius\Bundle\ApiBundle\Query\GetAddressLogEntryCollection; use Sylius\Component\Addressing\Model\AddressLogEntry; use Symfony\Component\Messenger\HandleTrait; use Symfony\Component\Messenger\MessageBusInterface; /** @experimental */ -final class GetAddressLogEntryAction +final class GetAddressLogEntryCollectionAction { use HandleTrait; @@ -34,7 +34,7 @@ final class GetAddressLogEntryAction { return $this->handle( $this->messageBus->dispatch( - new GetAddressLogEntry($id), + new GetAddressLogEntryCollection($id), ), ); } diff --git a/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php b/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntryCollection.php similarity index 92% rename from src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php rename to src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntryCollection.php index 794aad9051..f77d25ab94 100644 --- a/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntry.php +++ b/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntryCollection.php @@ -14,7 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Query; /** @experimental */ -final class GetAddressLogEntry +final class GetAddressLogEntryCollection { public function __construct( private int $addressId, diff --git a/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryCollectionHandler.php similarity index 83% rename from src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php rename to src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryCollectionHandler.php index dfbffffb0f..23b6047482 100644 --- a/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryHandler.php +++ b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryCollectionHandler.php @@ -15,12 +15,12 @@ namespace Sylius\Bundle\ApiBundle\QueryHandler; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Sylius\Bundle\ApiBundle\Query\GetAddressLogEntry; +use Sylius\Bundle\ApiBundle\Query\GetAddressLogEntryCollection; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\ResourceLogEntryRepositoryInterface; use Sylius\Component\Addressing\Model\AddressLogEntry; /** @experimental */ -final class GetAddressLogEntryHandler +final class GetAddressLogEntryCollectionHandler { public function __construct( private ResourceLogEntryRepositoryInterface $addressLogEntryRepository, @@ -28,7 +28,7 @@ final class GetAddressLogEntryHandler } /** @return Collection */ - public function __invoke(GetAddressLogEntry $query): Collection + public function __invoke(GetAddressLogEntryCollection $query): Collection { $queryBuilder = $this->addressLogEntryRepository->createByObjectIdQueryBuilder((string)$query->getAddressId()); diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml index a4b27133bd..8f44b3e6e4 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml @@ -64,7 +64,7 @@ GET /admin/addresses/{id}/log-entries - Sylius\Bundle\ApiBundle\Controller\GetAddressLogEntryAction + Sylius\Bundle\ApiBundle\Controller\GetAddressLogEntryCollectionAction admin:address:log_entry:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml index fde69c9dab..e5499f53f2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/controllers.xml @@ -55,7 +55,7 @@ - + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml index aa75caeb6f..c75bba42f4 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml @@ -25,7 +25,7 @@ - + diff --git a/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryCollectionHandlerSpec.php similarity index 83% rename from src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php rename to src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryCollectionHandlerSpec.php index ac4632bde5..1edc59a3ee 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryCollectionHandlerSpec.php @@ -17,10 +17,10 @@ use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\QueryBuilder; use Gedmo\Loggable\LogEntryInterface; use PhpSpec\ObjectBehavior; -use Sylius\Bundle\ApiBundle\Query\GetAddressLogEntry; +use Sylius\Bundle\ApiBundle\Query\GetAddressLogEntryCollection; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\ResourceLogEntryRepositoryInterface; -final class GetAddressLogEntryHandlerSpec extends ObjectBehavior +final class GetAddressLogEntryCollectionHandlerSpec extends ObjectBehavior { function let(ResourceLogEntryRepositoryInterface $addressLogEntryRepository): void { @@ -38,6 +38,6 @@ final class GetAddressLogEntryHandlerSpec extends ObjectBehavior $queryBuilder->getQuery()->willReturn($query); $addressLogEntryRepository->createByObjectIdQueryBuilder('3')->willReturn($queryBuilder); - $this(new GetAddressLogEntry(3))->shouldIterateAs([$addressLogEntryOne, $addressLogEntryTwo]); + $this(new GetAddressLogEntryCollection(3))->shouldIterateAs([$addressLogEntryOne, $addressLogEntryTwo]); } } From a903bc077dac1fd9097081a2bec7355cd83a5fb2 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 12 Dec 2023 20:56:49 +0100 Subject: [PATCH 18/44] [API] Add missing shipping method serialization and resources --- .../config/api_resources/ShippingMethod.xml | 57 ++++++++++++++++++- .../api_resources/ShippingMethodRule.xml | 34 +++++++++++ .../Resources/config/app/config.yaml | 1 + .../config/serialization/ShippingMethod.xml | 6 ++ .../serialization/ShippingMethodRule.xml | 33 +++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml create mode 100644 src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml index d482e8dc62..1970338757 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml @@ -16,7 +16,7 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - sylius + Sylius\Bundle\ShippingBundle\Validator\GroupsGenerator\ShippingMethodConfigurationGroupsGenerator ASC @@ -48,6 +48,33 @@ admin:shipping_method:read + + +Example configuration for `total_weight_greater_than_or_equal` rule type: + +``` +{ + "type": "total_weight_greater_than_or_equal", + "configuration": { + "weight": int + } +} +``` + +Example configuration for `order_total_greater_than_or_equal` rule type: + +``` +{ + "type": "order_total_greater_than_or_equal", + "configuration": { + "channel-code": [ + "amount": int, + ] + } +} +``` + + @@ -118,6 +145,33 @@ admin:shipping_method:read + + +Example configuration for `total_weight_greater_than_or_equal` rule type: + +``` +{ + "type": "total_weight_greater_than_or_equal", + "configuration": { + "weight": int + } +} +``` + +Example configuration for `order_total_greater_than_or_equal` rule type: + +``` +{ + "type": "order_total_greater_than_or_equal", + "configuration": { + "channel-code": [ + "amount": int, + ] + } +} +``` + + @@ -133,6 +187,7 @@ + object diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml new file mode 100644 index 0000000000..ddde51741f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml @@ -0,0 +1,34 @@ + + + + + + + admin + + + + GET + ApiPlatform\Core\Action\NotFoundAction + false + false + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml index 3deb7801f2..361c073aa3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml @@ -15,6 +15,7 @@ parameters: - "%sylius.security.new_api_route%/admin/channel-pricings/{id}" - "%sylius.security.new_api_route%/admin/promotion-actions/{id}" - "%sylius.security.new_api_route%/admin/promotion-rules/{id}" + - "%sylius.security.new_api_route%/admin/shipping-method-rules/{id}" - "%sylius.security.new_api_route%/admin/shop-users/{id}" sylius.security.new_api_route: "/api/v2" sylius.security.new_api_regex: "^%sylius.security.new_api_route%" diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml index 282969744d..a07ccd72eb 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml @@ -78,6 +78,12 @@ admin:shipping_method:update + + admin:shipping_method:read + admin:shipping_method:create + admin:shipping_method:update + + admin:shipping_method:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml new file mode 100644 index 0000000000..c061165bd0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml @@ -0,0 +1,33 @@ + + + + + + + + admin:shipping_method:read + + + admin:shipping_method:read + admin:shipping_method:create + admin:shipping_method:update + + + admin:shipping_method:read + admin:shipping_method:create + admin:shipping_method:update + + + From cc77a334a12a8734b628e225115332a65298f91f Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 12 Dec 2023 20:58:02 +0100 Subject: [PATCH 19/44] Validate shipping method rules and calculators --- .../Extension/ShippingMethodTypeExtension.php | 13 +++ .../Resources/config/services/form.xml | 1 + .../Calculator/FlatRateConfigurationType.php | 8 -- .../PerUnitRateConfigurationType.php | 8 -- ...ghtGreaterThanOrEqualConfigurationType.php | 8 -- ...WeightLessThanOrEqualConfigurationType.php | 8 -- .../Resources/config/services.xml | 4 +- .../Resources/config/services/validator.xml | 34 ++++++ .../config/validation/ShippingMethod.xml | 33 ++++++ .../config/validation/ShippingMethodRule.xml | 57 ++++++++++ .../Resources/translations/validators.en.yml | 3 + .../ShippingMethodCalculatorExists.php | 31 ++++++ .../Constraint/ShippingMethodRule.php | 31 ++++++ ...pingMethodConfigurationGroupsGenerator.php | 43 ++++++++ ...hippingMethodCalculatorExistsValidator.php | 46 ++++++++ .../Validator/ShippingMethodRuleValidator.php | 61 +++++++++++ ...MethodConfigurationGroupsGeneratorSpec.php | 63 +++++++++++ ...ingMethodCalculatorExistsValidatorSpec.php | 67 ++++++++++++ .../ShippingMethodRuleValidatorSpec.php | 101 ++++++++++++++++++ 19 files changed, 585 insertions(+), 35 deletions(-) create mode 100644 src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml create mode 100644 src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml create mode 100644 src/Sylius/Bundle/ShippingBundle/Validator/Constraint/ShippingMethodCalculatorExists.php create mode 100644 src/Sylius/Bundle/ShippingBundle/Validator/Constraint/ShippingMethodRule.php create mode 100644 src/Sylius/Bundle/ShippingBundle/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGenerator.php create mode 100644 src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php create mode 100644 src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php create mode 100644 src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php create mode 100644 src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php create mode 100644 src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php index 7d3044fc99..c4c3293285 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php @@ -16,13 +16,19 @@ namespace Sylius\Bundle\CoreBundle\Form\Extension; use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType; use Sylius\Bundle\ChannelBundle\Form\Type\ChannelChoiceType; use Sylius\Bundle\ShippingBundle\Form\Type\ShippingMethodType; +use Sylius\Bundle\ShippingBundle\Validator\GroupsGenerator\ShippingMethodConfigurationGroupsGenerator; use Sylius\Bundle\TaxationBundle\Form\Type\TaxCategoryChoiceType; use Sylius\Component\Core\Model\Scope; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; final class ShippingMethodTypeExtension extends AbstractTypeExtension { + public function __construct(private ShippingMethodConfigurationGroupsGenerator $configurationGroupsGenerator) + { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder @@ -43,6 +49,13 @@ final class ShippingMethodTypeExtension extends AbstractTypeExtension ; } + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'validation_groups' => $this->configurationGroupsGenerator, + ]); + } + public function getExtendedType(): string { return ShippingMethodType::class; diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml index fe6be70e19..6d30383539 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml @@ -113,6 +113,7 @@ + diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php index fd4f3b5b3a..5de03df6d1 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php @@ -17,9 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class FlatRateConfigurationType extends AbstractType { @@ -28,11 +25,6 @@ final class FlatRateConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.shipping_calculator.flat_rate_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Range(['min' => 0, 'minMessage' => 'sylius.shipping_method.calculator.min', 'groups' => ['sylius']]), - new Type(['type' => 'integer', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php index 239dcda8f9..3fc36ff8c0 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php @@ -17,9 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class PerUnitRateConfigurationType extends AbstractType { @@ -28,11 +25,6 @@ final class PerUnitRateConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.shipping_calculator.per_unit_rate_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Range(['min' => 0, 'minMessage' => 'sylius.shipping_method.calculator.min', 'groups' => ['sylius']]), - new Type(['type' => 'integer', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php index 4ce271baa2..7d0fd0be7f 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\ShippingBundle\Form\Type\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\GreaterThan; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class TotalWeightGreaterThanOrEqualConfigurationType extends AbstractType { @@ -29,11 +26,6 @@ final class TotalWeightGreaterThanOrEqualConfigurationType extends AbstractType NumberType::class, [ 'label' => 'sylius.form.shipping_method_rule.weight', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - new GreaterThan(['value' => 0, 'groups' => ['sylius']]), - ], ], ); } diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php index 34d962fcc1..b826dff6aa 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\ShippingBundle\Form\Type\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\GreaterThan; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class TotalWeightLessThanOrEqualConfigurationType extends AbstractType { @@ -29,11 +26,6 @@ final class TotalWeightLessThanOrEqualConfigurationType extends AbstractType NumberType::class, [ 'label' => 'sylius.form.shipping_method_rule.weight', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - new GreaterThan(['value' => 0, 'groups' => ['sylius']]), - ], ], ); } diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml index 5e730cc453..dcb5eed30c 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml @@ -14,9 +14,7 @@ - - - + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml new file mode 100644 index 0000000000..1139556a99 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml @@ -0,0 +1,34 @@ + + + + + + + + %sylius.shipping_calculators% + + + + + %sylius.shipping_method_rules% + %sylius.shipping.shipping_method_rule.validation_groups% + + + + + %sylius.shipping.shipping_method_calculator.validation_groups% + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml index 62d19a9ee0..4498f29bc7 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml @@ -34,6 +34,39 @@ + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml new file mode 100644 index 0000000000..8d04870b48 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml index 8a91ee53e9..80627ae6fc 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml @@ -16,6 +16,7 @@ sylius: calculator: min: 'Shipping charge cannot be lower than 0.' not_blank: 'Please select shipping method calculator.' + invalid_calculator: 'Invalid calculator. Available calculators are {{ available_calculators }}.' name: max_length: 'Shipping method name must not be longer than {{ limit }} characters.' min_length: 'Shipping method name must be at least {{ limit }} characters long.' @@ -26,6 +27,8 @@ sylius: unique: 'The shipping method with given code already exists.' zone: not_blank: 'Please select shipping method zone.' + rule: + invalid_type: 'Invalid rule type. Available rule types are {{ available_rule_types }}.' shipment: shipping_method: diff --git a/src/Sylius/Bundle/ShippingBundle/Validator/Constraint/ShippingMethodCalculatorExists.php b/src/Sylius/Bundle/ShippingBundle/Validator/Constraint/ShippingMethodCalculatorExists.php new file mode 100644 index 0000000000..3b1ae4a277 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Validator/Constraint/ShippingMethodCalculatorExists.php @@ -0,0 +1,31 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + /** + * @param FormInterface|ShippingMethodInterface $object + * + * @return array + */ + public function __invoke($object): array + { + if ($object instanceof FormInterface) { + $object = $object->getData(); + } + + Assert::isInstanceOf($object, ShippingMethodInterface::class); + + return $this->validationGroups[$object->getCalculator()] ?? ['sylius']; + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php new file mode 100644 index 0000000000..81757148a5 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php @@ -0,0 +1,46 @@ + $calculators */ + public function __construct(private array $calculators) + { + } + + /** @param string|null $value */ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof ShippingMethodCalculatorExists) { + throw new UnexpectedTypeException($constraint, ShippingMethodCalculatorExists::class); + } + + if ($value === null || $value === '') { + return; + } + + if (!in_array($value, array_keys($this->calculators), true)) { + $this->context->buildViolation($constraint->invalidShippingCalculator) + ->setParameter('{{ available_calculators }}', implode(', ', array_keys($this->calculators))) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php new file mode 100644 index 0000000000..b0b832613a --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php @@ -0,0 +1,61 @@ + $ruleTypes + * @param array> $validationGroups + */ + public function __construct( + private array $ruleTypes, + private array $validationGroups, + ) { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$value instanceof ShippingMethodRuleInterface) { + throw new UnexpectedValueException($value, ShippingMethodRuleInterface::class); + } + + if (!$constraint instanceof ShippingMethodRule) { + throw new UnexpectedTypeException($constraint, ShippingMethodRule::class); + } + + $type = $value->getType(); + if (!array_key_exists($type, $this->ruleTypes)) { + $this->context->buildViolation($constraint->invalidType) + ->setParameter('{{ available_rule_types }}', implode(', ', array_keys($this->ruleTypes))) + ->atPath('type') + ->addViolation() + ; + + return; + } + + /** @var string[] $groups */ + $groups = $this->validationGroups[$type] ?? $constraint->groups; + $validator = $this->context->getValidator()->inContext($this->context); + $validator->validate(value: $value, groups: $groups); + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php b/src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php new file mode 100644 index 0000000000..f9fd2028af --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php @@ -0,0 +1,63 @@ +beConstructedWith([ + 'flat_rate' => ['rate', 'sylius'], + 'per_unit_rate' => ['rate', 'sylius'], + ]); + } + + function it_throws_error_if_invalid_object_is_passed(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new \stdClass()]) + ; + } + + function it_returns_shipping_method_configuration_validation_groups( + ShippingMethodInterface $shippingMethod, + ): void { + $shippingMethod->getCalculator()->willReturn('flat_rate'); + + $this($shippingMethod)->shouldReturn(['rate', 'sylius']); + } + + function it_returns_default_validation_groups( + ShippingMethodInterface $shippingMethod, + ): void { + $shippingMethod->getCalculator()->willReturn(null); + + $this($shippingMethod)->shouldReturn(['sylius']); + } + + function it_returns_gateway_config_validation_groups_if_it_is_shipping_method_form( + FormInterface $form, + ShippingMethodInterface $shippingMethod, + ): void { + $form->getData()->willReturn($shippingMethod); + $shippingMethod->getCalculator()->willReturn('per_unit_rate'); + + $this($form)->shouldReturn(['rate', 'sylius']); + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php new file mode 100644 index 0000000000..9b97189556 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php @@ -0,0 +1,67 @@ +beConstructedWith( + [ + 'flat_rate' => 'sylius.form.shipping_calculator.flat_rate_configuration.label', + 'per_unit_rate' => 'sylius.form.shipping_calculator.per_unit_rate_configuration.label', + ], + ); + + $this->initialize($executionContext); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_shipping_method_calculator_exists( + Constraint $constraint, + ShippingMethodRuleInterface $shippingMethodRule, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$shippingMethodRule, $constraint]) + ; + } + + function it_adds_violation_to_wrong_shipping_method_calculator( + ExecutionContextInterface $executionContext, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $executionContext->buildViolation((new ShippingMethodCalculatorExists())->invalidShippingCalculator)->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate('wrong_calculator', new ShippingMethodCalculatorExists()); + } + + function it_does_not_add_violation_to_correct_shipping_method_calculator(ExecutionContextInterface $executionContext): void + { + $executionContext->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->validate('flat_rate', new ShippingMethodCalculatorExists()); + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php new file mode 100644 index 0000000000..65c3bb6d35 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php @@ -0,0 +1,101 @@ +beConstructedWith( + [ + 'total_weight_greater_than_or_equal' => 'sylius.form.shipping_method_rule.total_weight_greater_than_or_equal', + 'order_total_greater_than_or_equal' => 'sylius.form.shipping_method_rule.items_total_greater_than_or_equal', + 'different_rule' => 'sylius.form.shipping_method_rule.different_rule', + ], + [ + 'total_weight_greater_than_or_equal' => ['sylius', 'total_weight'], + 'order_total_greater_than_or_equal' => ['sylius', 'order_total'], + ], + ); + + $this->initialize($executionContext); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_shipping_method_rule( + Constraint $constraint, + ShippingMethodRuleInterface $shippingMethodRule, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$shippingMethodRule, $constraint]) + ; + } + + function it_adds_violation_to_shipping_method_rule_with_wrong_type( + ExecutionContextInterface $executionContext, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ShippingMethodRuleInterface $shippingMethodRule, + ): void { + $shippingMethodRule->getType()->willReturn('wrong_rule'); + $executionContext->buildViolation((new ShippingMethodRule())->invalidType)->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->atPath(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($shippingMethodRule, new ShippingMethodRule()); + } + + function it_calls_a_validator_with_group( + ExecutionContextInterface $executionContext, + ShippingMethodRuleInterface $shippingMethodRule, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $shippingMethodRule->getType()->willReturn('total_weight_greater_than_or_equal'); + $executionContext->getValidator()->willReturn($validator); + $validator->inContext($executionContext)->willReturn($contextualValidator); + + $contextualValidator->validate($shippingMethodRule, null, ['sylius', 'total_weight'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($shippingMethodRule, new ShippingMethodRule(['groups' => ['sylius', 'total_weight']])); + } + + function it_calls_validator_with_default_groups_if_none_assigned_to_shipping_method_rule( + ExecutionContextInterface $executionContext, + ShippingMethodRuleInterface $shippingMethodRule, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $shippingMethodRule->getType()->willReturn('different_rule'); + + $executionContext->getValidator()->willReturn($validator); + $validator->inContext($executionContext)->willReturn($contextualValidator); + + $contextualValidator->validate($shippingMethodRule, null, ['sylius'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($shippingMethodRule, new ShippingMethodRule(['groups' => ['sylius']])); + } +} From 19c04d301f549ba9e0833f4e6a2dd545a1ea7b8a Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 12 Dec 2023 20:59:08 +0100 Subject: [PATCH 20/44] [Behat] Cover shipping method management --- .../adding_shipping_method_with_rule.feature | 12 +- .../editing_shipping_method.feature | 8 - .../shipping_method_code_validation.feature | 15 +- ..._flat_rate_per_shipment_validation.feature | 4 +- ...thod_flat_rate_per_unit_validation.feature | 4 +- .../shipping_method_validation.feature | 35 ++- .../Admin/ManagingShippingMethodsContext.php | 200 ++++++++++++++++++ .../Admin/ManagingShippingMethodsContext.php | 57 ++++- .../Page/Admin/ShippingMethod/CreatePage.php | 42 ++-- .../ShippingMethod/CreatePageInterface.php | 9 +- 10 files changed, 332 insertions(+), 54 deletions(-) diff --git a/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature b/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature index 623daae995..07c4fb45fa 100644 --- a/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature +++ b/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature @@ -10,7 +10,7 @@ Feature: Adding a new shipping method with rule And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Adding a new shipping method with total weight greater than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -24,7 +24,7 @@ Feature: Adding a new shipping method with rule Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry - @ui @javascript + @ui @javascript @api Scenario: Adding a new shipping method with total weight less than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -38,7 +38,7 @@ Feature: Adding a new shipping method with rule Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry - @ui @javascript + @ui @javascript @api Scenario: Adding a new shipping method with order total greater than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -47,12 +47,12 @@ Feature: Adding a new shipping method with rule And I define it for the zone named "United States" And I choose "Flat rate per shipment" calculator And I specify its amount as 50 for "Web-US" channel - And I add the "Items total greater than or equal" rule configured with $200 for "Web-US" channel + And I add the Items total greater than or equal rule configured with $200 for "Web-US" channel And I add it Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry - @ui @javascript + @ui @javascript @api Scenario: Adding a new shipping method with order total less than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -61,7 +61,7 @@ Feature: Adding a new shipping method with rule And I define it for the zone named "United States" And I choose "Flat rate per shipment" calculator And I specify its amount as 50 for "Web-US" channel - And I add the "Items total less than or equal" rule configured with $200 for "Web-US" channel + And I add the Items total less than or equal rule configured with $200 for "Web-US" channel And I add it Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry diff --git a/features/shipping/managing_shipping_methods/editing_shipping_method.feature b/features/shipping/managing_shipping_methods/editing_shipping_method.feature index 63b9c64f7c..df12ec9df8 100644 --- a/features/shipping/managing_shipping_methods/editing_shipping_method.feature +++ b/features/shipping/managing_shipping_methods/editing_shipping_method.feature @@ -10,14 +10,6 @@ Feature: Editing shipping method And the store allows shipping with "UPS Carrier" identified by "UPS_CARRIER" And I am logged in as an administrator - @todo - Scenario: Trying to change shipping method code - When I want to modify a shipping method "UPS Carrier" - And I change its code to "UPS" - And I save my changes - Then I should be notified that code cannot be changed - And shipping method "UPS Carrier" should still have code "UPS_CARRIER" - @ui @api Scenario: Seeing disabled code field when editing shipping method When I want to modify a shipping method "UPS Carrier" diff --git a/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature index 543a3c8cf6..f902f01a24 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature @@ -10,7 +10,7 @@ Feature: Shipping method code validation And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new shipping method with special symbols in the code When I want to create a new shipping method And I name it "FedEx Carrier" in "English (United States)" @@ -19,7 +19,7 @@ Feature: Shipping method code validation Then I should be notified that code needs to contain only specific symbols And shipping method with name "FedEx Carrier" should not be added - @ui + @ui @api Scenario: Trying to add a new shipping method with spaces in the code When I want to create a new shipping method And I name it "FedEx Carrier" in "English (United States)" @@ -27,14 +27,3 @@ Feature: Shipping method code validation And I try to add it Then I should be notified that code needs to contain only specific symbols And shipping method with name "FedEx Carrier" should not be added - - @ui @javascript - Scenario: Trying to add a new shipping method with - When I want to create a new shipping method - And I name it "FedEx Carrier First US Division" in "English (United States)" - And I specify its code as "PEC-US_01" - And I define it for the zone named "United States" - And I choose "Flat rate per shipment" calculator - And I specify its amount as 50 for "Web-US" channel - And I add it - Then the shipping method "FedEx Carrier First US Division" should appear in the registry diff --git a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature index 9675d60b92..ac638f5396 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature @@ -10,7 +10,7 @@ Feature: Shipping method flat rate per shipment calculator validation And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per shipment calculator without specifying its amount When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -21,7 +21,7 @@ Feature: Shipping method flat rate per shipment calculator validation Then I should be notified that amount for "Web" channel should not be blank And shipping method with name "FedEx Carrier" should not be added - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per shipment calculator with charge below 0 When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" diff --git a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature index 1a5ff7c8fb..b5afb7ffa6 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature @@ -10,7 +10,7 @@ Feature: Shipping method flat rate per unit calculator validation And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per unit calculator without specifying its amount When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -21,7 +21,7 @@ Feature: Shipping method flat rate per unit calculator validation Then I should be notified that amount for "Web" channel should not be blank And shipping method with name "FedEx Carrier" should not be added - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per unit calculator with charge below 0 When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" diff --git a/features/shipping/managing_shipping_methods/shipping_method_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_validation.feature index af8f37c9c1..16c9a7b8e2 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_validation.feature @@ -5,7 +5,8 @@ Feature: Shipping method validation I want to be prevented from adding it without specifying required fields Background: - Given the store operates on a single channel in "United States" + Given the store operates on a channel named "Web-US" in "USD" currency + And the store has a zone "United States" with code "US" And the store is available in "English (United States)" And I am logged in as an administrator @@ -46,10 +47,38 @@ Feature: Shipping method validation Then I should be notified that name is required And this shipping method should still be named "UPS Ground" - @ui + @ui @api Scenario: Trying to remove zone from existing shipping method Given the store allows shipping with "UPS Ground" When I want to modify this shipping method And I remove its zone And I try to save my changes - Then I should be notified that zone has to be selected + Then I should be notified that the zone is required + + @ui @javascript @api + Scenario: Adding a new shipping method with order total greater than or equal rule that contains invalid data + When I want to create a new shipping method + And I specify its code as "FED_EX_CARRIER" + And I specify its position as 0 + And I name it "FedEx Carrier" in "English (United States)" + And I define it for the zone named "United States" + And I choose "Flat rate per shipment" calculator + And I specify its amount as 50 for "Web-US" channel + And I add the "Total weight greater than or equal" rule configured with invalid data + And I add it + Then I should be notified that the weight rule has an invalid configuration + And the shipping method "FedEx Carrier" should not appear in the registry + + @ui @javascript @api + Scenario: Adding a new shipping method with order total less than or equal rule that contains invalid data + When I want to create a new shipping method + And I specify its code as "FED_EX_CARRIER" + And I specify its position as 0 + And I name it "FedEx Carrier" in "English (United States)" + And I define it for the zone named "United States" + And I choose "Flat rate per shipment" calculator + And I specify its amount as 50 for "Web-US" channel + And I add the "Items total less than or equal" rule configured with invalid data for "Web-US" channel + And I add it + Then I should be notified that the amount rule has an invalid configuration in "Web-US" channel + And the shipping method "FedEx Carrier" should not appear in the registry diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php index 6aec1d6aaa..85c648eede 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php @@ -24,6 +24,7 @@ use Sylius\Component\Addressing\Model\ZoneInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ShippingMethodInterface; +use Sylius\Component\Shipping\Calculator\DefaultCalculators; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Webmozart\Assert\Assert; @@ -63,6 +64,94 @@ final class ManagingShippingMethodsContext implements Context ]); } + /** + * @When I add the :rule rule configured with :weight + */ + public function iAddTheRuleConfiguredWithWeight(string $rule, int $weight): void + { + match ($rule) { + 'Total weight less than or equal' => $this->client->addRequestData('rules', [ + [ + 'type' => 'total_weight_less_than_or_equal', + 'configuration' => [ + 'weight' => $weight, + ], + ], + ]), + 'Total weight greater than or equal' => $this->client->addRequestData('rules', [ + [ + 'type' => 'total_weight_greater_than_or_equal', + 'configuration' => [ + 'weight' => $weight, + ], + ], + ]), + default => throw new \InvalidArgumentException('Unsupported shipping method rule'), + }; + } + + /** + * @When I add the "Total weight greater than or equal" rule configured with invalid data + */ + public function iAddTheTotalWeightGreaterThanOrEqualRuleConfiguredWithInvalidData(): void + { + $this->client->addRequestData('rules', [ + [ + 'type' => 'total_weight_greater_than_or_equal', + 'configuration' => [ + 'weight' => true, + ], + ], + ]); + } + + /** + * @When /^I add the ([^"]+) rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ + */ + public function iAddTheRuleConfiguredWithForChannel(string $rule, int $value, ChannelInterface $channel): void + { + match ($rule) { + 'Items total less than or equal' => $this->client->addRequestData('rules', [ + [ + 'type' => 'order_total_less_than_or_equal', + 'configuration' => [ + $channel->getCode() => [ + 'amount' => $value, + ], + ], + ], + ]), + 'Items total greater than or equal' => $this->client->addRequestData('rules', [ + [ + 'type' => 'order_total_greater_than_or_equal', + 'configuration' => [ + $channel->getCode() => [ + 'amount' => $value, + ], + ], + ], + ]), + default => throw new \InvalidArgumentException('Unsupported shipping method rule'), + }; + } + + /** + * @When /^I add the "Items total less than or equal" rule configured with invalid data for ("[^"]+" channel)$/ + */ + public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWithInvalidData(ChannelInterface $channel): void + { + $this->client->addRequestData('rules', [ + [ + 'type' => 'order_total_greater_than_or_equal', + 'configuration' => [ + $channel->getCode() => [ + 'amount' => true, + ], + ], + ], + ]); + } + /** * @When I change my locale to :localeCode */ @@ -123,6 +212,29 @@ final class ManagingShippingMethodsContext implements Context ]); } + /** + * @When I do not specify amount for :calculatorName calculator + */ + public function iDoNotSpecifyAmountForCalculator(string $calculatorName): void + { + match ($calculatorName) { + 'Flat rate per shipment' => $this->client->addRequestData('calculator', DefaultCalculators::FLAT_RATE), + 'Flat rate per unit' => $this->client->addRequestData('calculator', DefaultCalculators::PER_UNIT_RATE), + 'default' => throw new \InvalidArgumentException('Unsupported calculator name'), + }; + + $channelCode = $this->sharedStorage->get('channel')->getCode(); + $this->client->addRequestData('configuration', [$channelCode => ['amount' => null]]); + } + + /** + * @When I remove its zone + */ + public function iRemoveItsZone(): void + { + $this->client->replaceRequestData('zone', null); + } + /** * @When I try to show :shippingMethod shipping method */ @@ -310,6 +422,17 @@ final class ManagingShippingMethodsContext implements Context ); } + /** + * @Then the shipping method :name should not appear in the registry + */ + public function theShippingMethodShouldNotAppearInTheRegistry(string $name): void + { + Assert::false( + $this->responseChecker->hasItemWithTranslation($this->client->index(Resources::SHIPPING_METHODS), 'en_US', 'name', $name), + sprintf('Shipping method with name %s exists', $name), + ); + } + /** * @Then /^(this shipping method) should still be in the registry$/ */ @@ -527,6 +650,17 @@ final class ManagingShippingMethodsContext implements Context ); } + /** + * @Then I should be notified that the zone is required + */ + public function iShouldBeNotifiedThatZoneHasToBeIriAndCannotBeNull(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Expected IRI or nested document for attribute "zone", "NULL" given.', + ); + } + /** * @Then shipping method with :element :value should not be added */ @@ -578,6 +712,72 @@ final class ManagingShippingMethodsContext implements Context // Intentionally left blank } + /** + * @Then shipping method :shippingMethod should still have code :code + */ + public function shippingMethodShouldStillHaveCode(ShippingMethodInterface $shippingMethod, string $code): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::SHIPPING_METHODS, $shippingMethod->getCode()), 'code'), + $code, + ); + } + + /** + * @Then I should be notified that amount for :channel channel should not be blank + */ + public function iShouldBeNotifiedThatAmountForChannelShouldNotBeBlank(ChannelInterface $channel): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('configuration[%s][amount]: This value should not be blank.', $channel->getCode()), + ); + } + + /** + * @Then I should be notified that code needs to contain only specific symbols + */ + public function iShouldBeNotifiedThatCodeNeedsToContainOnlySpecificSymbols(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'code: Shipping method code can only be comprised of letters, numbers, dashes and underscores.', + ); + } + + /** + * @Then I should be notified that shipping charge for :channel channel cannot be lower than 0 + */ + public function iShouldBeNotifiedThatShippingChargeForChannelCannotBeLowerThan0(ChannelInterface $channel): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('configuration[%s][amount]: Shipping charge cannot be lower than 0.', $channel->getCode()), + ); + } + + /** + * @Then I should be notified that the weight rule has an invalid configuration + */ + public function iShouldBeNotifiedThatTheWeightRuleHasAnInvalidConfiguration(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'configuration[weight]: This value should be of type numeric.', + ); + } + + /** + * @Then I should be notified that the amount rule has an invalid configuration in :channel channel + */ + public function iShouldBeNotifiedThatTheAmountRuleHasAnInvalidConfigurationInChannel(ChannelInterface $channel): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('configuration[%s][amount]: This value should be of type numeric.', $channel->getCode()), + ); + } + private function getAdminLocaleCode(): string { /** @var AdminUserInterface $adminUser */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php index 7ee6ed75d6..aa2550a719 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php @@ -146,6 +146,16 @@ final class ManagingShippingMethodsContext implements Context Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $shipmentMethodName])); } + /** + * @Then the shipping method :shipmentMethodName should not appear in the registry + */ + public function theShipmentMethodShouldNotAppearInTheRegistry(string $shipmentMethodName): void + { + $this->iWantToBrowseShippingMethods(); + + Assert::false($this->indexPage->isSingleResourceOnPage(['name' => $shipmentMethodName])); + } + /** * @Given /^(this shipping method) should still be in the registry$/ */ @@ -235,7 +245,7 @@ final class ManagingShippingMethodsContext implements Context /** * @Then I should be notified that code needs to contain only specific symbols */ - public function iShouldBeNotifiedThatCodeShouldContain() + public function iShouldBeNotifiedThatCodeNeedsToContainOnlySpecificSymbols(): void { $this->assertFieldValidationMessage( 'code', @@ -324,8 +334,9 @@ final class ManagingShippingMethodsContext implements Context /** * @Then I should be notified that :element has to be selected + * @Then I should be notified that the :element is required */ - public function iShouldBeNotifiedThatElementHasToBeSelected($element) + public function iShouldBeNotifiedThatElementHasToBeSelected(string $element): void { $this->assertFieldValidationMessage($element, sprintf('Please select shipping method %s.', $element)); } @@ -484,6 +495,15 @@ final class ManagingShippingMethodsContext implements Context $this->createPage->fillRuleOption('Weight', (string) $weight); } + /** + * @When I add the "Total weight greater than or equal" rule configured with invalid data + */ + public function iAddTheTotalWeightGreaterThanOrEqualRuleConfiguredWithInvalidData(): void + { + $this->createPage->addRule('Total weight greater than or equal'); + $this->createPage->fillRuleOption('Weight', 'invalid data'); + } + /** * @When I add the "Total weight less than or equal" rule configured with :weight */ @@ -495,6 +515,7 @@ final class ManagingShippingMethodsContext implements Context /** * @When /^I add the "Items total greater than or equal" rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ + * @When /^I add the Items total greater than or equal rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ */ public function iAddTheItemsTotalGreaterThanOrEqualRuleConfiguredWith($value, ChannelInterface $channel): void { @@ -504,6 +525,7 @@ final class ManagingShippingMethodsContext implements Context /** * @When /^I add the "Items total less than or equal" rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ + * @When /^I add the Items total less than or equal rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ */ public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWith($value, ChannelInterface $channel): void { @@ -511,6 +533,15 @@ final class ManagingShippingMethodsContext implements Context $this->createPage->fillRuleOptionForChannel($channel->getCode(), 'Amount', (string) $value); } + /** + * @When /^I add the "Items total less than or equal" rule configured with invalid data for ("[^"]+" channel)$/ + */ + public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWithInvalidData(ChannelInterface $channel): void + { + $this->createPage->addRule('Items total less than or equal'); + $this->createPage->fillRuleOptionForChannel($channel->getCode(), 'Amount', 'Invalid data'); + } + /** * @When /^I remove the shipping charges of ("[^"]+" channel)$/ */ @@ -532,6 +563,28 @@ final class ManagingShippingMethodsContext implements Context ); } + /** + * @Then I should be notified that the weight rule has an invalid configuration + */ + public function iShouldBeNotifiedThatTheWeightRuleHasAnInvalidConfiguration(): void + { + $this->assertFieldValidationMessage('weight', 'Please enter a number.'); + } + + /** + * @Then I should be notified that the amount rule has an invalid configuration in :channel channel + */ + public function iShouldBeNotifiedThatTheAmountRuleHasAnInvalidConfigurationInChannel(ChannelInterface $channel): void + { + /** @var CreatePageInterface|UpdatePageInterface $currentPage */ + $currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]); + + Assert::same( + $currentPage->getValidationMessageForRuleAmount($channel->getCode()), + 'Please enter a valid money amount.', + ); + } + /** * @param string $element * @param string $expectedMessage diff --git a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php index 7074de8681..595b4f55b9 100644 --- a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php +++ b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php @@ -75,21 +75,15 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface public function getValidationMessageForAmount(string $channelCode): string { $foundElement = $this->getFieldElement('amount', ['%channelCode%' => $channelCode]); - if (null === $foundElement) { - throw new ElementNotFoundException($this->getSession(), 'Field element'); - } - $validationMessage = $foundElement->find('css', '.sylius-validation-error'); - if (null === $validationMessage) { - throw new ElementNotFoundException( - $this->getSession(), - 'Validation message', - 'css', - '.sylius-validation-error', - ); - } + return $this->getValidationMessageForElement($foundElement); + } - return $validationMessage->getText(); + public function getValidationMessageForRuleAmount(string $channelCode): string + { + $foundElement = $this->getChannelConfigurationOfLastRule($channelCode); + + return $this->getValidationMessageForElement($foundElement); } public function addRule(string $ruleName): void @@ -126,6 +120,7 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface 'channel' => '#sylius_shipping_method_channels .ui.checkbox:contains("%channel%")', 'calculator' => '#sylius_shipping_method_calculator', 'calculator_configuration' => '.ui.segment.configuration', + 'weight' => '[id*="sylius_shipping_method_rules_"][id*="_configuration_weight"]', 'code' => '#sylius_shipping_method_code', 'name' => '#sylius_shipping_method_translations_en_US_name', 'zone' => '#sylius_shipping_method_zone', @@ -133,6 +128,25 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface ]); } + private function getValidationMessageForElement(NodeElement $element): string + { + if (null === $element) { + throw new ElementNotFoundException($this->getSession(), 'Field element'); + } + + $validationMessage = $element->find('css', '.sylius-validation-error'); + if (null === $validationMessage) { + throw new ElementNotFoundException( + $this->getSession(), + 'Validation message', + 'css', + '.sylius-validation-error', + ); + } + + return $validationMessage->getText(); + } + /** * @throws ElementNotFoundException */ @@ -167,7 +181,7 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface return $items; } - private function getChannelConfigurationOfLastRule(string $channelCode): NodeElement + private function getChannelConfigurationOfLastRule(string $channelCode): ?NodeElement { return $this ->getLastCollectionItem('rules') diff --git a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php index 4612a1d529..aee0845836 100644 --- a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php @@ -32,13 +32,14 @@ interface CreatePageInterface extends BaseCreatePageInterface public function chooseCalculator(string $name): void; - public function checkChannel($channelName): void; + public function checkChannel(string $channelName): void; - /** - * @throws ElementNotFoundException - */ + /** @throws ElementNotFoundException */ public function getValidationMessageForAmount(string $channelCode): string; + /** @throws ElementNotFoundException */ + public function getValidationMessageForRuleAmount(string $channelCode): string; + public function addRule(string $ruleName): void; public function selectRuleOption(string $option, string $value, bool $multiple = false): void; From 387e59a5f77c9a7e65471799297b484eeca596a5 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 12 Dec 2023 21:01:00 +0100 Subject: [PATCH 21/44] Add parameter for shipping method rule and calculator validation groups --- config/packages/_sylius.yaml | 22 +++++++++++++ .../DependencyInjection/Configuration.php | 8 +++++ .../SyliusShippingExtension.php | 3 ++ .../SyliusShippingExtensionTest.php | 32 +++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/config/packages/_sylius.yaml b/config/packages/_sylius.yaml index e21717f004..bae92163d8 100644 --- a/config/packages/_sylius.yaml +++ b/config/packages/_sylius.yaml @@ -67,6 +67,28 @@ sylius_promotion: - 'sylius' - 'sylius_promotion_rule_item_total' +sylius_shipping: + shipping_method_rules_validation_groups: + total_weight_greater_than_or_equal: + - 'sylius' + - 'total_weight' + total_weight_less_than_or_equal: + - 'sylius' + - 'total_weight' + order_total_greater_than_or_equal: + - 'sylius' + - 'order_total' + order_total_less_than_or_equal: + - 'sylius' + - 'order_total' + shipping_method_calculators_validation_groups: + flat_rate: + - 'sylius' + - 'rate' + per_unit_rate: + - 'sylius' + - 'rate' + sylius_shop: product_grid: include_all_descendants: true diff --git a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php index 70728065e8..2ccae83d1f 100644 --- a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php @@ -49,6 +49,14 @@ final class Configuration implements ConfigurationInterface $rootNode ->addDefaultsIfNotSet() ->children() + ->arrayNode('shipping_method_rules_validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->arrayNode('shipping_method_calculators_validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() ->end() ; diff --git a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php index bb18e8aa5d..9155272cb3 100644 --- a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php +++ b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php @@ -33,6 +33,9 @@ final class SyliusShippingExtension extends AbstractResourceExtension $this->registerResources('sylius', $config['driver'], $config['resources'], $container); + $container->setParameter('sylius.shipping.shipping_method_rules.validation_groups', $config['shipping_method_rules_validation_groups']); + $container->setParameter('sylius.shipping.shipping_method_calculators.validation_groups', $config['shipping_method_calculators_validation_groups']); + $loader->load('services.xml'); $this->registerAutoconfiguration($container); } diff --git a/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php b/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php index f65e03505b..381e36cc9c 100644 --- a/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php +++ b/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php @@ -99,6 +99,38 @@ final class SyliusShippingExtensionTest extends AbstractExtensionTestCase ); } + /** @test */ + public function it_loads_shipping_method_rules_validation_groups_parameter_value_properly(): void + { + $this->load(['shipping_method_rules_validation_groups' => [ + 'total_weight_greater_than_or_equal' => ['sylius', 'total_weight'], + 'order_total_greater_than_or_equal' => ['sylius'], + 'order_total_less_than_or_equal' => ['sylius', 'order_total'], + 'total_weight_less_than_or_equal' => ['sylius'], + ]]); + + $this->assertContainerBuilderHasParameter('sylius.shipping.shipping_method_rules.validation_groups', [ + 'total_weight_greater_than_or_equal' => ['sylius', 'total_weight'], + 'order_total_greater_than_or_equal' => ['sylius'], + 'order_total_less_than_or_equal' => ['sylius', 'order_total'], + 'total_weight_less_than_or_equal' => ['sylius'], + ]); + } + + /** @test */ + public function it_loads_shipping_method_calculators_validation_groups_parameter_value_properly(): void + { + $this->load(['shipping_method_calculators_validation_groups' => [ + 'flat_rate' => ['sylius'], + 'per_unit_rate' => ['sylius', 'per_unit_rate'], + ]]); + + $this->assertContainerBuilderHasParameter('sylius.shipping.shipping_method_calculators.validation_groups', [ + 'flat_rate' => ['sylius'], + 'per_unit_rate' => ['sylius', 'per_unit_rate'], + ]); + } + protected function getContainerExtensions(): array { return [new SyliusShippingExtension()]; From cc5e95560d359f8e54b2092742354db7319df60a Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 12 Dec 2023 21:03:14 +0100 Subject: [PATCH 22/44] Cover contract tests --- tests/Api/Admin/ShippingMethodsTest.php | 243 ++++++++++++++++++ .../Api/DataFixtures/ORM/shipping_method.yaml | 4 + .../archive_shipping_method_response.json | 4 + .../get_shipping_method_response.json | 4 + .../get_shipping_methods_response.json | 8 + .../restore_shipping_method_response.json | 5 + ...hod_calculator_configuration_response.json | 34 +++ ...update_shipping_method_rules_response.json | 81 ++++++ ...r_shipping_method_calculator_response.json | 28 ++ ...ion_for_shipping_method_rule_response.json | 38 +++ ...ype_for_shipping_method_rule_response.json | 13 + 11 files changed, 462 insertions(+) create mode 100644 tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_calculator_configuration_response.json create mode 100644 tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_rules_response.json create mode 100644 tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_calculator_response.json create mode 100644 tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_rule_response.json create mode 100644 tests/Api/Responses/Expected/admin/shipping_method/wrong_type_for_shipping_method_rule_response.json diff --git a/tests/Api/Admin/ShippingMethodsTest.php b/tests/Api/Admin/ShippingMethodsTest.php index f0394ab6ed..d54ab4c93e 100644 --- a/tests/Api/Admin/ShippingMethodsTest.php +++ b/tests/Api/Admin/ShippingMethodsTest.php @@ -161,4 +161,247 @@ final class ShippingMethodsTest extends JsonApiTestCase Response::HTTP_UNPROCESSABLE_ENTITY, ); } + + /** @test */ + public function it_does_not_update_shipping_methods_rules_with_wrong_configuration(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'country.yaml', + 'shipping_method.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ShippingMethodInterface $shippingMethod */ + $shippingMethod = $fixtures['shipping_method_ups']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/shipping-methods/%s', $shippingMethod->getCode()), + server: $header, + content: json_encode([ + 'rules' => [ + [ + 'type' => 'total_weight_greater_than_or_equal', + 'configuration' => [ + 'weight' => 'wrong_value', + ], + ], + [ + 'type' => 'total_weight_less_than_or_equal', + 'configuration' => [ + 'weight' => 'wrong_value', + ], + ], + [ + 'type' => 'order_total_greater_than_or_equal', + 'configuration' => [ + 'MOBILE' => [ + 'amount' => 'wrong_value', + ], + 'WEB' => [ + 'amount' => 'wrong_value', + ], + ], + ], + [ + 'type' => 'order_total_less_than_or_equal', + 'configuration' => [ + 'MOBILE' => [ + 'amount' => 'wrong_value', + ], + 'WEB' => [ + 'amount' => 'wrong_value', + ], + ], + ], + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/shipping_method/wrong_configuration_for_shipping_method_rule_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + /** @test */ + public function it_does_not_update_shipping_methods_rules_with_wrong_types(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'country.yaml', + 'shipping_method.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ShippingMethodInterface $shippingMethod */ + $shippingMethod = $fixtures['shipping_method_ups']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/shipping-methods/%s', $shippingMethod->getCode()), + server: $header, + content: json_encode([ + 'rules' => [ + [ + 'type' => 'wrong_type', + 'configuration' => [ + 'weight' => 123, + ], + ], + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/shipping_method/wrong_type_for_shipping_method_rule_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + /** @test */ + public function it_updates_a_shipping_method_rules(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'country.yaml', + 'shipping_method.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ShippingMethodInterface $shippingMethod */ + $shippingMethod = $fixtures['shipping_method_ups']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/shipping-methods/%s', $shippingMethod->getCode()), + server: $header, + content: json_encode([ + 'rules' => [ + [ + 'type' => 'total_weight_greater_than_or_equal', + 'configuration' => [ + 'weight' => 123, + ], + ], + [ + 'type' => 'total_weight_less_than_or_equal', + 'configuration' => [ + 'weight' => 123, + ], + ], + [ + 'type' => 'order_total_greater_than_or_equal', + 'configuration' => [ + 'MOBILE' => [ + 'amount' => 123, + ], + 'WEB' => [ + 'amount' => 123, + ], + ], + ], + [ + 'type' => 'order_total_less_than_or_equal', + 'configuration' => [ + 'MOBILE' => [ + 'amount' => 123, + ], + 'WEB' => [ + 'amount' => 123, + ], + ], + ], + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/shipping_method/update_shipping_method_rules_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_does_not_update_shipping_method_calculator_configuration_with_wrong_configuration(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'country.yaml', + 'shipping_method.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ShippingMethodInterface $shippingMethod */ + $shippingMethod = $fixtures['shipping_method_ups']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/shipping-methods/%s', $shippingMethod->getCode()), + server: $header, + content: json_encode([ + 'calculator' => 'per_unit_rate', + 'configuration' => [ + 'WEB' => [ + 'amount' => 'wrong_value', + ], + 'WRONG_CODE' => [ + 'amount' => 'wrong_value', + ], + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/shipping_method/wrong_configuration_for_shipping_method_calculator_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + /** @test */ + public function it_updates_shipping_method_calculator_configuration(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'country.yaml', + 'shipping_method.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ShippingMethodInterface $shippingMethod */ + $shippingMethod = $fixtures['shipping_method_ups']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/shipping-methods/%s', $shippingMethod->getCode()), + server: $header, + content: json_encode([ + 'calculator' => 'per_unit_rate', + 'calculatorConfiguration' => [ + 'WEB' => [ + 'amount' => 123, + ], + 'MOBILE' => [ + 'amount' => 123, + ], + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/shipping_method/update_shipping_method_calculator_configuration_response', + Response::HTTP_OK, + ); + } } diff --git a/tests/Api/DataFixtures/ORM/shipping_method.yaml b/tests/Api/DataFixtures/ORM/shipping_method.yaml index 927191645e..d84bc7b685 100644 --- a/tests/Api/DataFixtures/ORM/shipping_method.yaml +++ b/tests/Api/DataFixtures/ORM/shipping_method.yaml @@ -6,6 +6,8 @@ Sylius\Component\Core\Model\ShippingMethod: configuration: WEB: amount: 500 + MOBILE: + amount: 1000 zone: '@zone_world' currentLocale: 'en_US' translations: ["@shipping_method_translation_ups"] @@ -17,6 +19,8 @@ Sylius\Component\Core\Model\ShippingMethod: configuration: WEB: amount: 1000 + MOBILE: + amount: 2000 zone: '@zone_world' currentLocale: 'en_US' translations: ["@shipping_method_translation_dhl"] diff --git a/tests/Api/Responses/Expected/admin/shipping_method/archive_shipping_method_response.json b/tests/Api/Responses/Expected/admin/shipping_method/archive_shipping_method_response.json index 937aaafcc5..6bebf59af8 100644 --- a/tests/Api/Responses/Expected/admin/shipping_method/archive_shipping_method_response.json +++ b/tests/Api/Responses/Expected/admin/shipping_method/archive_shipping_method_response.json @@ -13,8 +13,12 @@ "configuration": { "WEB": { "amount": 500 + }, + "MOBILE": { + "amount": 1000 } }, + "rules": [], "zone": "\/api\/v2\/admin\/zones\/WORLD", "channels": [ "\/api\/v2\/admin\/channels\/WEB" diff --git a/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_method_response.json b/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_method_response.json index 4953aaf0cd..83d1cab2f5 100644 --- a/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_method_response.json +++ b/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_method_response.json @@ -12,8 +12,12 @@ "configuration": { "WEB": { "amount": 500 + }, + "MOBILE": { + "amount": 1000 } }, + "rules": [], "zone": "\/api\/v2\/admin\/zones\/WORLD", "channels": [ "\/api\/v2\/admin\/channels\/WEB" diff --git a/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_methods_response.json b/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_methods_response.json index aea1ea244d..766b4d635a 100644 --- a/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_methods_response.json +++ b/tests/Api/Responses/Expected/admin/shipping_method/get_shipping_methods_response.json @@ -16,8 +16,12 @@ "configuration": { "WEB": { "amount": 500 + }, + "MOBILE": { + "amount": 1000 } }, + "rules": [], "zone": "\/api\/v2\/admin\/zones\/WORLD", "channels": [ "\/api\/v2\/admin\/channels\/WEB" @@ -45,8 +49,12 @@ "configuration": { "WEB": { "amount": 1000 + }, + "MOBILE": { + "amount": 2000 } }, + "rules": [], "zone": "\/api\/v2\/admin\/zones\/WORLD", "channels": [ "\/api\/v2\/admin\/channels\/WEB" diff --git a/tests/Api/Responses/Expected/admin/shipping_method/restore_shipping_method_response.json b/tests/Api/Responses/Expected/admin/shipping_method/restore_shipping_method_response.json index 4953aaf0cd..141227b7b6 100644 --- a/tests/Api/Responses/Expected/admin/shipping_method/restore_shipping_method_response.json +++ b/tests/Api/Responses/Expected/admin/shipping_method/restore_shipping_method_response.json @@ -12,12 +12,17 @@ "configuration": { "WEB": { "amount": 500 + }, + "MOBILE": { + "amount": 1000 } + }, "zone": "\/api\/v2\/admin\/zones\/WORLD", "channels": [ "\/api\/v2\/admin\/channels\/WEB" ], + "rules": [], "translations": { "en_US": { "@id": "\/api\/v2\/admin\/shipping-method-translations\/@integer@", diff --git a/tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_calculator_configuration_response.json b/tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_calculator_configuration_response.json new file mode 100644 index 0000000000..56191ca346 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_calculator_configuration_response.json @@ -0,0 +1,34 @@ +{ + "@context": "\/api\/v2\/contexts\/ShippingMethod", + "@id": "\/api\/v2\/admin\/shipping-methods\/UPS", + "@type": "ShippingMethod", + "zone": "\/api\/v2\/admin\/zones\/WORLD", + "channels": [ + "\/api\/v2\/admin\/channels\/WEB" + ], + "id": @integer@, + "code": "UPS", + "position": 0, + "calculator": "per_unit_rate", + "configuration": { + "WEB": { + "amount": 500 + }, + "MOBILE": { + "amount": 1000 + } + }, + "rules": [], + "createdAt": "@date@", + "updatedAt": "@date@", + "enabled": true, + "translations": { + "en_US": { + "@id": "\/api\/v2\/admin\/shipping-method-translations\/@integer@", + "@type": "ShippingMethodTranslation", + "id": @integer@, + "name": "UPS", + "description": @string@ + } + } +} diff --git a/tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_rules_response.json b/tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_rules_response.json new file mode 100644 index 0000000000..7199d2096e --- /dev/null +++ b/tests/Api/Responses/Expected/admin/shipping_method/update_shipping_method_rules_response.json @@ -0,0 +1,81 @@ +{ + "@context": "\/api\/v2\/contexts\/ShippingMethod", + "@id": "\/api\/v2\/admin\/shipping-methods\/UPS", + "@type": "ShippingMethod", + "zone": "\/api\/v2\/admin\/zones\/WORLD", + "channels": [ + "\/api\/v2\/admin\/channels\/WEB" + ], + "id": @integer@, + "code": "UPS", + "position": 0, + "calculator": "flat_rate", + "configuration": { + "WEB": { + "amount": 500 + }, + "MOBILE": { + "amount": 1000 + } + }, + "rules": [ + { + "@id": "\/api\/v2\/admin\/shipping-method-rules\/@integer@", + "@type": "ShippingMethodRule", + "id": @integer@, + "type": "total_weight_greater_than_or_equal", + "configuration": { + "weight": 123 + } + }, + { + "@id": "\/api\/v2\/admin\/shipping-method-rules\/@integer@", + "@type": "ShippingMethodRule", + "id": @integer@, + "type": "total_weight_less_than_or_equal", + "configuration": { + "weight": 123 + } + }, + { + "@id": "\/api\/v2\/admin\/shipping-method-rules\/@integer@", + "@type": "ShippingMethodRule", + "id": @integer@, + "type": "order_total_greater_than_or_equal", + "configuration": { + "MOBILE": { + "amount": 123 + }, + "WEB": { + "amount": 123 + } + } + }, + { + "@id": "\/api\/v2\/admin\/shipping-method-rules\/@integer@", + "@type": "ShippingMethodRule", + "id": @integer@, + "type": "order_total_less_than_or_equal", + "configuration": { + "MOBILE": { + "amount": 123 + }, + "WEB": { + "amount": 123 + } + } + } + ], + "createdAt": "@date@", + "updatedAt": "@date@", + "enabled": true, + "translations": { + "en_US": { + "@id": "\/api\/v2\/admin\/shipping-method-translations\/@integer@", + "@type": "ShippingMethodTranslation", + "id": @integer@, + "name": "UPS", + "description": @string@ + } + } +} diff --git a/tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_calculator_response.json b/tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_calculator_response.json new file mode 100644 index 0000000000..d0d2578762 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_calculator_response.json @@ -0,0 +1,28 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "configuration[WEB][amount]: This value should be a valid number.\nconfiguration[WEB][amount]: This value should be of type numeric.\nconfiguration[MOBILE]: This field is missing.\nconfiguration[WRONG_CODE]: This field was not expected.", + "violations": [ + { + "propertyPath": "configuration[WEB][amount]", + "message": "This value should be a valid number.", + "code": "ad9a9798-7a99-4df7-8ce9-46e416a1e60b" + }, + { + "propertyPath": "configuration[WEB][amount]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + }, + { + "propertyPath": "configuration[MOBILE]", + "message": "This field is missing.", + "code": "2fa2158c-2a7f-484b-98aa-975522539ff8" + }, + { + "propertyPath": "configuration[WRONG_CODE]", + "message": "This field was not expected.", + "code": "7703c766-b5d5-4cef-ace7-ae0dd82304e9" + } + ] +} diff --git a/tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_rule_response.json b/tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_rule_response.json new file mode 100644 index 0000000000..6600148338 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/shipping_method/wrong_configuration_for_shipping_method_rule_response.json @@ -0,0 +1,38 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "rules[0].configuration[weight]: This value should be of type numeric.\nrules[1].configuration[weight]: This value should be of type numeric.\nrules[2].configuration[WEB][amount]: This value should be of type numeric.\nrules[2].configuration[MOBILE][amount]: This value should be of type numeric.\nrules[3].configuration[WEB][amount]: This value should be of type numeric.\nrules[3].configuration[MOBILE][amount]: This value should be of type numeric.", + "violations": [ + { + "propertyPath": "rules[0].configuration[weight]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + }, + { + "propertyPath": "rules[1].configuration[weight]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + }, + { + "propertyPath": "rules[2].configuration[WEB][amount]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + }, + { + "propertyPath": "rules[2].configuration[MOBILE][amount]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + }, + { + "propertyPath": "rules[3].configuration[WEB][amount]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + }, + { + "propertyPath": "rules[3].configuration[MOBILE][amount]", + "message": "This value should be of type numeric.", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" + } + ] +} diff --git a/tests/Api/Responses/Expected/admin/shipping_method/wrong_type_for_shipping_method_rule_response.json b/tests/Api/Responses/Expected/admin/shipping_method/wrong_type_for_shipping_method_rule_response.json new file mode 100644 index 0000000000..78d1a169dd --- /dev/null +++ b/tests/Api/Responses/Expected/admin/shipping_method/wrong_type_for_shipping_method_rule_response.json @@ -0,0 +1,13 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "rules[0].type: Invalid rule type. Available rule types are total_weight_greater_than_or_equal, total_weight_less_than_or_equal, order_total_greater_than_or_equal, order_total_less_than_or_equal.", + "violations": [ + { + "propertyPath": "rules[0].type", + "message": "Invalid rule type. Available rule types are total_weight_greater_than_or_equal, total_weight_less_than_or_equal, order_total_greater_than_or_equal, order_total_less_than_or_equal.", + "code": null + } + ] +} From c8b9ae343fe7052dbf772a58c67c463791125df3 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Thu, 14 Dec 2023 14:34:30 +0100 Subject: [PATCH 23/44] Change shipping validation groups parameters --- config/packages/_sylius.yaml | 42 ++++++++++--------- .../DependencyInjection/Configuration.php | 22 +++++++--- .../SyliusShippingExtension.php | 4 +- .../config/validation/ShippingMethod.xml | 8 ++-- .../config/validation/ShippingMethodRule.xml | 10 ++--- .../SyliusShippingExtensionTest.php | 30 +++++++------ 6 files changed, 66 insertions(+), 50 deletions(-) diff --git a/config/packages/_sylius.yaml b/config/packages/_sylius.yaml index bae92163d8..d383c4314a 100644 --- a/config/packages/_sylius.yaml +++ b/config/packages/_sylius.yaml @@ -68,26 +68,28 @@ sylius_promotion: - 'sylius_promotion_rule_item_total' sylius_shipping: - shipping_method_rules_validation_groups: - total_weight_greater_than_or_equal: - - 'sylius' - - 'total_weight' - total_weight_less_than_or_equal: - - 'sylius' - - 'total_weight' - order_total_greater_than_or_equal: - - 'sylius' - - 'order_total' - order_total_less_than_or_equal: - - 'sylius' - - 'order_total' - shipping_method_calculators_validation_groups: - flat_rate: - - 'sylius' - - 'rate' - per_unit_rate: - - 'sylius' - - 'rate' + shipping_method_rule: + validation_groups: + total_weight_greater_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_total_weight' + total_weight_less_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_total_weight' + order_total_greater_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_order_total' + order_total_less_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_order_total' + shipping_method_calculator: + validation_groups: + flat_rate: + - 'sylius' + - 'sylius_shipping_method_calculator_rate' + per_unit_rate: + - 'sylius' + - 'sylius_shipping_method_calculator_rate' sylius_shop: product_grid: diff --git a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php index 2ccae83d1f..b6340f8007 100644 --- a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php @@ -49,13 +49,23 @@ final class Configuration implements ConfigurationInterface $rootNode ->addDefaultsIfNotSet() ->children() - ->arrayNode('shipping_method_rules_validation_groups') - ->useAttributeAsKey('name') - ->variablePrototype()->end() + ->arrayNode('shipping_method_rule') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() ->end() - ->arrayNode('shipping_method_calculators_validation_groups') - ->useAttributeAsKey('name') - ->variablePrototype()->end() + ->arrayNode('shipping_method_calculator') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() ->end() ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() ->end() diff --git a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php index 9155272cb3..4166bb3262 100644 --- a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php +++ b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php @@ -33,8 +33,8 @@ final class SyliusShippingExtension extends AbstractResourceExtension $this->registerResources('sylius', $config['driver'], $config['resources'], $container); - $container->setParameter('sylius.shipping.shipping_method_rules.validation_groups', $config['shipping_method_rules_validation_groups']); - $container->setParameter('sylius.shipping.shipping_method_calculators.validation_groups', $config['shipping_method_calculators_validation_groups']); + $container->setParameter('sylius.shipping.shipping_method_rule.validation_groups', $config['shipping_method_rule']['validation_groups']); + $container->setParameter('sylius.shipping.shipping_method_calculator.validation_groups', $config['shipping_method_calculator']['validation_groups']); $loader->load('services.xml'); $this->registerAutoconfiguration($container); diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml index 4498f29bc7..c630949626 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml @@ -42,7 +42,7 @@ - + diff --git a/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json index d2e44f6f56..e5d091c003 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json @@ -15,7 +15,8 @@ "price": 2000, "originalPrice": null, "lowestPriceBeforeDiscount": null, - "minimumPrice": 0 + "minimumPrice": 0, + "appliedPromotions": [] } }, "translations": { diff --git a/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json b/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json index 70f60dde0f..f82ab40a32 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json @@ -19,7 +19,8 @@ "price": 2000, "originalPrice": null, "lowestPriceBeforeDiscount": null, - "minimumPrice": 0 + "minimumPrice": 0, + "appliedPromotions": [] } }, "translations": { @@ -59,7 +60,8 @@ "price": 3000, "originalPrice": null, "lowestPriceBeforeDiscount": null, - "minimumPrice": 0 + "minimumPrice": 0, + "appliedPromotions": [] } }, "translations": { diff --git a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response.json b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response.json index 15266dfe18..36712ba8a9 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response.json @@ -13,7 +13,8 @@ "price": 4000, "originalPrice": null, "minimumPrice": 0, - "lowestPriceBeforeDiscount": null + "lowestPriceBeforeDiscount": null, + "appliedPromotions": [] } }, "translations": { diff --git a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json index a441851282..e52a8824b3 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json @@ -15,7 +15,8 @@ "price": 4000, "originalPrice": 5000, "minimumPrice": 2000, - "lowestPriceBeforeDiscount": null + "lowestPriceBeforeDiscount": null, + "appliedPromotions": [] } }, "translations": { diff --git a/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json index b7b9c8b5d3..29331c6fbd 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json @@ -15,7 +15,8 @@ "price": 3000, "originalPrice": 4000, "lowestPriceBeforeDiscount": 2000, - "minimumPrice": 500 + "minimumPrice": 500, + "appliedPromotions": [] } }, "translations": { From d42453e58dd30ce6ef4a05a67d82934c68e7645f Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 11 Dec 2023 17:33:36 +0100 Subject: [PATCH 30/44] [API][Admin] Refactor viewing product suite split --- ...cts_in_the_shop_while_viewing_them.feature | 2 +- ...tions_details_for_a_simple_product.feature | 3 +- ..._promotions_details_within_variant.feature | 3 +- ...before_discount_for_simple_product.feature | 2 +- ...ice_before_discount_within_variant.feature | 2 +- ..._non_translatable_attributes_admin.feature | 6 +- ...ct_attributes_in_different_locales.feature | 4 +- ...that_has_taxon_excluded_on_channel.feature | 1 - ...CatalogPromotionProductVariantsContext.php | 81 +++++++++++++++++++ .../Admin/ManagingProductVariantsContext.php | 67 --------------- .../Api/Admin/ManagingProductsContext.php | 13 +++ .../Ui/Admin/ProductShowPageContext.php | 2 +- .../Product/ShowPage/AttributesElement.php | 2 +- .../viewing_product_in_admin_panel.yaml | 18 +++++ .../suites/api/product/viewing_products.yml | 5 +- .../viewing_product_in_admin_panel.yaml | 11 ++- .../suites/ui/product/viewing_products.yml | 2 - 17 files changed, 138 insertions(+), 86 deletions(-) diff --git a/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature b/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature index 1462355658..adb92da5f9 100644 --- a/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature +++ b/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Checking products in the shop while viewing them In order to check a product in shop in all channels it is available in As an Administrator diff --git a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature index b4ba826c10..af103dfa62 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Seeing applied catalog promotions details for a simple product In order to be aware of simple product price change reason As an Administrator @@ -13,6 +13,7 @@ Feature: Seeing applied catalog promotions details for a simple product And it reduces price by "90%" And it is enabled And I am logged in as an administrator + And I am browsing products @ui @no-api Scenario: Seeing applied catalog promotion details on a simple product diff --git a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature index d908bb6bc5..8994cca64e 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Seeing applied catalog promotions details within variant In order to be aware of variant's price change reason As an Administrator @@ -20,6 +20,7 @@ Feature: Seeing applied catalog promotions details within variant And it reduces price by "37%" And it is enabled And I am logged in as an administrator + And I am browsing products @ui @no-api Scenario: Seeing applied catalog promotion details within variant diff --git a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature index b841b02235..ef1421c307 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Seeing the lowest price before the discount for a simple product In order to be aware of simple product prices As an Administrator diff --git a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature index d4064f0c7f..c7e7acf1f4 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Seeing the lowest price before the discount within variant In order to be aware of variant's prices As an Administrator diff --git a/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature b/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature index c9324be56f..b48af9a17c 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Viewing product's non translatable attributes In order to see product's non translatable attribute As an Administrator @@ -14,9 +14,9 @@ Feature: Viewing product's non translatable attributes And I am logged in as an administrator And I am browsing products - @ui + @ui @api Scenario: Viewing product's non translatable attributes along with default ones When I access "Iron Pickaxe" product page - Then I should see non-translatable attribute "crit chance" with value "10 %" + Then I should see non-translatable attribute "crit chance" with value 10% And I should see attribute "Material" with value "Iron" in "English (United States)" locale And I should see attribute "Material" with value "Żelazo" in "Polish (Poland)" locale diff --git a/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature b/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature index 26410e8f8c..a7a97098ca 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Viewing product's attributes in different locales In order to see product's specification in all locales As a Administrator @@ -15,7 +15,7 @@ Feature: Viewing product's attributes in different locales And I am logged in as an administrator And I am browsing products - @ui + @ui @api Scenario: Viewing product's attributes defined in different locales When I access "Iron Shield" product page Then I should see attribute "material" with value "oak wood" in "English (United States)" locale diff --git a/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature b/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature index 1fed8eead7..bb90bfff11 100644 --- a/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature +++ b/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature @@ -14,7 +14,6 @@ Feature: Not seeing the lowest price for a product that has a taxon excluded on And the store also has a product "Cauliflower" priced at "$25.00" And it belongs to "Vegetables" and "Special offers" And this product's price changed to "$15.00" and original price changed to "$25.00" - And I am logged in as an administrator @api @ui Scenario: Not seeing the lowest price for a product that has a taxon excluded on the channel diff --git a/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php index a29591fb38..a90157a874 100644 --- a/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php @@ -19,6 +19,9 @@ use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Component\Core\Model\CatalogPromotionInterface; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Webmozart\Assert\Assert; final class BrowsingCatalogPromotionProductVariantsContext implements Context @@ -40,6 +43,17 @@ final class BrowsingCatalogPromotionProductVariantsContext implements Context $this->client->filter(); } + /** + * @When /^I want to view all variants of (this product)$/ + * @When /^I view(?:| all) variants of the (product "[^"]+")$/ + */ + public function iWantToViewAllVariantsOfThisProduct(ProductInterface $product): void + { + $this->client->index(Resources::PRODUCT_VARIANTS); + $this->client->addFilter('product', $this->iriConverter->getIriFromResource($product)); + $this->client->filter(); + } + /** * @Then /^there should be (\d+) product variants? on the list$/ */ @@ -66,4 +80,71 @@ final class BrowsingCatalogPromotionProductVariantsContext implements Context )); } } + + /** + * @Then :variant variant price should be decreased by catalog promotion :catalogPromotion in :channel channel + */ + public function variantPriceShouldBeDecreasedByCatalogPromotion( + ProductVariantInterface $variant, + CatalogPromotionInterface $catalogPromotion, + ChannelInterface $channel, + ): void { + Assert::true( + $this->variantHasCatalogPromotionInChannel($variant, $catalogPromotion, $channel), + sprintf( + 'Catalog promotion "%s" was not found in applied promotions of variant "%s" in channel "%s".', + $catalogPromotion->getCode(), + $variant->getCode(), + $channel->getCode(), + ), + ); + } + + /** + * @Then :variant variant price should not be decreased by catalog promotion :catalogPromotion in :channel channel + */ + public function variantPriceShouldNotBeDecreasedByCatalogPromotion( + ProductVariantInterface $variant, + CatalogPromotionInterface $catalogPromotion, + ChannelInterface $channel, + ): void { + Assert::false( + $this->variantHasCatalogPromotionInChannel($variant, $catalogPromotion, $channel), + sprintf( + 'Catalog promotion "%s" was found in applied promotions of variant "%s" in channel "%s".', + $catalogPromotion->getCode(), + $variant->getCode(), + $channel->getCode(), + ), + ); + } + + private function variantHasCatalogPromotionInChannel( + ProductVariantInterface $variant, + CatalogPromotionInterface $catalogPromotion, + ChannelInterface $channel + ): bool { + $variantData = $this->getDataOfVariantWithCode($variant->getCode()); + + $promotions = $variantData['channelPricings'][$channel->getCode()]['appliedPromotions'] ?? []; + foreach ($promotions as $promotion) { + if ($promotion['code'] === $catalogPromotion->getCode()) { + return true; + } + } + + return false; + } + + private function getDataOfVariantWithCode(string $code): array + { + $variantsData = $this->responseChecker->getCollection($this->client->getLastResponse()); + foreach ($variantsData as $variantData) { + if ($variantData['code'] === $code) { + return $variantData; + } + } + + throw new \InvalidArgumentException(sprintf('Variant with code "%s" was not found.', $code)); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 179387f8af..45531fb75f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -733,71 +733,4 @@ final class ManagingProductVariantsContext implements Context 'On hand must be greater than the number of on hold units', ); } - - /** - * @Then :variant variant price should be decreased by catalog promotion :catalogPromotion in :channel channel - */ - public function variantPriceShouldBeDecreasedByCatalogPromotion( - ProductVariantInterface $variant, - CatalogPromotionInterface $catalogPromotion, - ChannelInterface $channel, - ): void { - Assert::true( - $this->variantHasCatalogPromotionInChannel($variant, $catalogPromotion, $channel), - sprintf( - 'Catalog promotion "%s" was not found in applied promotions of variant "%s" in channel "%s".', - $catalogPromotion->getCode(), - $variant->getCode(), - $channel->getCode(), - ), - ); - } - - /** - * @Then :variant variant price should not be decreased by catalog promotion :catalogPromotion in :channel channel - */ - public function variantPriceShouldNotBeDecreasedByCatalogPromotion( - ProductVariantInterface $variant, - CatalogPromotionInterface $catalogPromotion, - ChannelInterface $channel, - ): void { - Assert::false( - $this->variantHasCatalogPromotionInChannel($variant, $catalogPromotion, $channel), - sprintf( - 'Catalog promotion "%s" was found in applied promotions of variant "%s" in channel "%s".', - $catalogPromotion->getCode(), - $variant->getCode(), - $channel->getCode(), - ), - ); - } - - private function variantHasCatalogPromotionInChannel( - ProductVariantInterface $variant, - CatalogPromotionInterface $catalogPromotion, - ChannelInterface $channel - ): bool { - $variantData = $this->getDataOfVariantWithCode($variant->getCode()); - - $promotions = $variantData['channelPricings'][$channel->getCode()]['appliedPromotions'] ?? []; - foreach ($promotions as $promotion) { - if ($promotion['code'] === $catalogPromotion->getCode()) { - return true; - } - } - - return false; - } - - private function getDataOfVariantWithCode(string $code): array - { - $variantsData = $this->responseChecker->getCollection($this->client->getLastResponse()); - foreach ($variantsData as $variantData) { - if ($variantData['code'] === $code) { - return $variantData; - } - } - - throw new \InvalidArgumentException(sprintf('Variant with code "%s" was not found.', $code)); - } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index 0d028a8d34..aecfca717f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -25,6 +25,7 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\TaxonInterface; +use Sylius\Component\Locale\Model\LocaleInterface; use Sylius\Component\Product\Model\ProductAttributeInterface; use Sylius\Component\Product\Model\ProductOptionInterface; use Symfony\Component\HttpFoundation\Response; @@ -738,6 +739,7 @@ final class ManagingProductsContext implements Context /** * @Then I should see non-translatable attribute :attribute with value :value% + * @Then I should see non-translatable attribute :attribute with value :value % */ public function iShouldSeeNonTranslatableAttributeWithValue(ProductAttributeInterface $attribute, int $value): void { @@ -859,6 +861,17 @@ final class ManagingProductsContext implements Context Assert::notEmpty($this->responseChecker->getValue($this->client->getLastResponse(), 'images')); } + /** + * @Then I should see attribute :attribute with value :value in :locale locale + */ + public function iShouldSeeAttributeWithValueInLocale( + ProductAttributeInterface $attribute, + string $value, + LocaleInterface $locale, + ): void { + $this->hasAttributeWithValueInLastResponse($attribute, $value, $locale->getCode()); + } + private function getAdminLocaleCode(): string { /** @var AdminUserInterface $adminUser */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php b/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php index ec570a5344..19d0634cde 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php @@ -432,7 +432,7 @@ final class ProductShowPageContext implements Context } /** - * @Then I should see non-translatable attribute :attribute with value :value + * @Then /^I should see non-translatable attribute "([^"]+)" with value ([^"]+)%$/ */ public function iShouldSeeNonTranslatableAttributeWithValue(string $attribute, string $value): void { diff --git a/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php b/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php index e43e66b672..a364cbc74d 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php @@ -30,7 +30,7 @@ final class AttributesElement extends Element implements AttributesElementInterf { $attributeValue = $this->getDocument()->find('css', sprintf('.ui.segment[data-tab="non-translatable"] tr:contains("%s") td:nth-child(2)', $attribute))->getText(); - return $attributeValue === $value; + return str_contains($attributeValue, $value); } protected function getDefinedElements(): array diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml index 38b88f38e4..0d795243ba 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml @@ -8,20 +8,38 @@ default: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.channel + - sylius.behat.context.transform.currency - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_association_type + - sylius.behat.context.transform.product_attribute - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_option_value + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_category + - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.taxon + - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.setup.admin_api_security - sylius.behat.context.setup.channel + - sylius.behat.context.setup.locale - sylius.behat.context.setup.product - sylius.behat.context.setup.product_association + - sylius.behat.context.setup.product_attribute - sylius.behat.context.setup.product_option + - sylius.behat.context.setup.product_taxon - sylius.behat.context.setup.shipping_category + - sylius.behat.context.setup.taxation - sylius.behat.context.setup.taxonomy + - Sylius\Behat\Context\Setup\CatalogPromotionContext + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.api.admin.managing_products + - Sylius\Behat\Context\Api\Admin\BrowsingCatalogPromotionProductVariantsContext filters: tags: "@viewing_product_in_admin_panel&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml index 04f52873a7..ea9bd61c89 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml @@ -18,8 +18,8 @@ default: - sylius.behat.context.transform.taxon - Sylius\Behat\Context\Transform\CatalogPromotionContext - - sylius.behat.context.setup.admin_user - - sylius.behat.context.setup.admin_api_security +# - sylius.behat.context.setup.admin_user +# - sylius.behat.context.setup.admin_api_security - sylius.behat.context.setup.channel - sylius.behat.context.setup.customer - sylius.behat.context.setup.locale @@ -34,7 +34,6 @@ default: - Sylius\Behat\Context\Setup\PriceHistoryContext - sylius.behat.context.setup.calendar - - sylius.behat.context.api.admin.managing_product_variants - sylius.behat.context.api.shop.channel - sylius.behat.context.api.shop.product - sylius.behat.context.api.shop.product_attribute diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml index 43ea193ae1..71fb8bc930 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml @@ -9,7 +9,9 @@ default: - sylius.behat.context.hook.session - sylius.behat.context.transform.channel + - sylius.behat.context.transform.currency - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale - sylius.behat.context.transform.product - sylius.behat.context.transform.product_association_type - sylius.behat.context.transform.product_option @@ -19,19 +21,26 @@ default: - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.taxon + - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.channel + - sylius.behat.context.setup.locale - sylius.behat.context.setup.product - sylius.behat.context.setup.product_association + - sylius.behat.context.setup.product_attribute - sylius.behat.context.setup.product_option - sylius.behat.context.setup.product_taxon - sylius.behat.context.setup.shipping_category - - sylius.behat.context.setup.taxonomy - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.taxonomy + - Sylius\Behat\Context\Setup\CatalogPromotionContext + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.ui.admin.managing_product_attributes - sylius.behat.context.ui.admin.product_showpage + - sylius.behat.context.ui.shop.browsing_product filters: tags: "@viewing_product_in_admin_panel&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml index 308f23979b..cb9be9cee7 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml @@ -44,8 +44,6 @@ default: - Sylius\Behat\Context\Setup\PriceHistoryContext - sylius.behat.context.setup.calendar - - sylius.behat.context.ui.admin.managing_product_attributes - - sylius.behat.context.ui.admin.product_showpage - sylius.behat.context.ui.channel - sylius.behat.context.ui.shop.locale - sylius.behat.context.ui.shop.product From 237eb2ebc1beccafab76b80ab93e217ba797a966 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Tue, 12 Dec 2023 16:08:13 +0100 Subject: [PATCH 31/44] [API] Finish covering viewing product attributes --- .../viewing_select_product_attributes.feature | 12 ++--- .../Api/Shop/ProductAttributeContext.php | 28 +++++++++- .../Context/Setup/ProductAttributeContext.php | 53 +++++++++++++++++++ .../ManagingProductAttributesContext.php | 40 -------------- .../Resources/config/services/contexts/ui.xml | 1 - .../suites/api/product/viewing_products.yml | 3 +- 6 files changed, 87 insertions(+), 50 deletions(-) diff --git a/features/product/viewing_products/viewing_select_product_attributes.feature b/features/product/viewing_products/viewing_select_product_attributes.feature index a3d30d2f27..6e5e6f013c 100644 --- a/features/product/viewing_products/viewing_select_product_attributes.feature +++ b/features/product/viewing_products/viewing_select_product_attributes.feature @@ -17,24 +17,24 @@ Feature: Viewing product's select attributes Then I should see the product attribute "T-Shirt material" with value "Banana skin" on the list And I should also see the product attribute "T-Shirt material" with value "Cotton" on the list - @ui + @ui @api Scenario: Viewing a detailed page with product's select attribute after changing a value Given this product has select attribute "T-Shirt material" with values "Banana skin" and "Cotton" - When the administrator changes this product attribute's value "Cotton" to "Orange skin" + When this product attribute's value changed from "Cotton" to "Orange skin" And I check this product's details Then I should see the product attribute "T-Shirt material" with value "Banana skin" on the list And I should also see the product attribute "T-Shirt material" with value "Orange skin" on the list - @ui @javascript + @ui @javascript @api @no-postgres Scenario: Viewing a detailed page with product's select attribute after removing an only value Given this product has select attribute "T-Shirt material" with value "Cotton" - When the administrator deletes the value "Cotton" from this product attribute + When this product attribute's value "Cotton" has been removed And I check this product's details Then I should not see the product attribute "T-Shirt material" - @ui @javascript + @ui @javascript @api @no-postgres Scenario: Viewing a detailed page with product's select attribute after removing one of the value Given this product has select attribute "T-Shirt material" with values "Banana skin" and "Cotton" - When the administrator deletes the value "Cotton" from this product attribute + When this product attribute's value "Cotton" has been removed And I check this product's details Then I should see the product attribute "T-Shirt material" with value "Banana skin" diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php index 53c5f0d467..6eef5b2521 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php @@ -37,8 +37,15 @@ final class ProductAttributeContext implements Context public function iShouldSeeTheProductAttributeWithValue(string $attributeName, string $expectedAttribute): void { $attribute = $this->getAttributeByName($attributeName); + $attributeValue = $attribute['value']; - Assert::same($attribute['value'], $expectedAttribute); + if (is_array($attributeValue)) { + Assert::inArray($expectedAttribute, $attributeValue); + + return; + } + + Assert::same($attributeValue, $expectedAttribute); } /** @@ -71,6 +78,14 @@ final class ProductAttributeContext implements Context Assert::inArray($expectedAttribute, $attribute['value']); } + /** + * @Then I should not see the product attribute :attributeName + */ + public function iShouldNotSeeTheProductAttribute(string $attributeName): void + { + Assert::false($this->hasAttributeByName($attributeName)); + } + /** * @Then I should (also) see the product attribute :attributeName with date :expectedAttribute */ @@ -113,6 +128,17 @@ final class ProductAttributeContext implements Context Assert::same($attribute['name'], $name); } + private function hasAttributeByName(string $name): bool + { + foreach ($this->getAttributes() as $attribute) { + if ($attribute['name'] === $name) { + return true; + } + } + + return false; + } + private function getAttributeByName(string $name): array { foreach ($this->getAttributes() as $attribute) { diff --git a/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php b/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php index e17ee95caa..8210c99d3d 100644 --- a/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php @@ -423,6 +423,59 @@ final class ProductAttributeContext implements Context $this->objectManager->flush(); } + /** + * @When /^(this product attribute)'s value changed from "([^"]+)" to "([^"]+)"$/ + */ + public function thisAttributeValueChangedFromTo( + ProductAttributeInterface $attribute, + string $from, + string $to, + ): void { + $configuration = $attribute->getConfiguration(); + $choices = $configuration['choices'] ?? []; + + foreach ($choices as $uuid => $choice) { + foreach ($choice as $localeCode => $item) { + if ($item === $from) { + $choices[$uuid][$localeCode] = $to; + + break 2; + } + } + } + + $configuration['choices'] = $choices; + $attribute->setConfiguration($configuration); + + $this->objectManager->flush(); + } + + /** + * @When /^(this product attribute)'s value "([^"]+)" has been removed$/ + */ + public function thisAttributeValueHasBeenRemoved( + ProductAttributeInterface $attribute, + string $value, + ): void { + $configuration = $attribute->getConfiguration(); + $choices = $configuration['choices'] ?? []; + + foreach ($choices as $uuid => $choice) { + foreach ($choice as $item) { + if ($value === $item) { + unset($choices[$uuid]); + + break 2; + } + } + } + + $configuration['choices'] = $choices; + $attribute->setConfiguration($configuration); + + $this->objectManager->flush(); + } + private function createProductAttribute( string $type, string $name, diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php index 2bbff3bfa8..cbc2367993 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php @@ -18,8 +18,6 @@ use Sylius\Behat\Page\Admin\Crud\IndexPageInterface; use Sylius\Behat\Page\Admin\ProductAttribute\CreatePageInterface; use Sylius\Behat\Page\Admin\ProductAttribute\UpdatePageInterface; use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface; -use Sylius\Behat\Service\SharedSecurityServiceInterface; -use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Product\Model\ProductAttributeInterface; use Webmozart\Assert\Assert; @@ -30,7 +28,6 @@ final class ManagingProductAttributesContext implements Context private IndexPageInterface $indexPage, private UpdatePageInterface $updatePage, private CurrentPageResolverInterface $currentPageResolver, - private SharedSecurityServiceInterface $sharedSecurityService, ) { } @@ -233,25 +230,6 @@ final class ManagingProductAttributesContext implements Context $this->indexPage->open(); } - /** - * @When /^(the administrator) changes (this product attribute)'s value "([^"]*)" to "([^"]*)"$/ - */ - public function theAdministratorChangesThisProductAttributesValueTo( - AdminUserInterface $user, - ProductAttributeInterface $productAttribute, - string $oldValue, - string $newValue, - ): void { - $this->sharedSecurityService->performActionAsAdminUser( - $user, - function () use ($productAttribute, $oldValue, $newValue) { - $this->iWantToEditThisAttribute($productAttribute); - $this->iChangeItsValueTo($oldValue, $newValue); - $this->iSaveMyChanges(); - }, - ); - } - /** * @When I specify its min length as :min * @When I specify its min entries value as :min @@ -286,24 +264,6 @@ final class ManagingProductAttributesContext implements Context // Intentionally left blank to fulfill context expectation } - /** - * @When /^(the administrator) deletes the value "([^"]+)" from (this product attribute)$/ - */ - public function theAdministratorDeletesTheValueFromThisProductAttribute( - AdminUserInterface $user, - string $value, - ProductAttributeInterface $productAttribute, - ): void { - $this->sharedSecurityService->performActionAsAdminUser( - $user, - function () use ($productAttribute, $value) { - $this->iWantToEditThisAttribute($productAttribute); - $this->iDeleteValue($value); - $this->iSaveMyChanges(); - }, - ); - } - /** * @When I check (also) the :productAttributeName product attribute */ diff --git a/src/Sylius/Behat/Resources/config/services/contexts/ui.xml b/src/Sylius/Behat/Resources/config/services/contexts/ui.xml index 713fce8db6..821056a791 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/ui.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/ui.xml @@ -207,7 +207,6 @@ - diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml index ea9bd61c89..5ff6356353 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml @@ -18,8 +18,7 @@ default: - sylius.behat.context.transform.taxon - Sylius\Behat\Context\Transform\CatalogPromotionContext -# - sylius.behat.context.setup.admin_user -# - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.admin_user - sylius.behat.context.setup.channel - sylius.behat.context.setup.customer - sylius.behat.context.setup.locale From e86dd2fdcc10f6f3b4cc6ca5768cd04a98ceec60 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Thu, 14 Dec 2023 14:02:09 +0100 Subject: [PATCH 32/44] [API][Admin] Cover managing product associations --- ...g_associations_to_existing_product.feature | 8 +- .../adding_product_with_associations.feature | 2 +- .../changing_associations_of_product.feature | 22 +- .../ManagingProductAssociationsContext.php | 189 +++++++++++++ src/Sylius/Behat/Context/Api/Resources.php | 2 + .../Setup/ProductAssociationContext.php | 2 + .../ProductAssociationTypeContext.php | 1 + .../config/services/contexts/api/admin.xml | 9 + .../suites/api/product/managing_products.yml | 1 + .../api_resources/ProductAssociation.xml | 61 +++- .../config/serialization/Product.xml | 1 + .../serialization/ProductAssociation.xml | 9 + .../Resources/config/services/filters.xml | 8 + .../config/validation/ProductAssociation.xml | 38 +++ .../Resources/translations/validators.en.yml | 6 + .../Product/Model/ProductAssociation.php | 1 + tests/Api/Admin/ProductAssociationsTest.php | 265 ++++++++++++++++++ .../Api/DataFixtures/ORM/product/product.yaml | 24 +- .../product/products_with_associations.yaml | 13 + .../admin/product/get_product_response.json | 3 + .../admin/product/get_products_response.json | 5 + .../admin/product/post_product_response.json | 1 + .../admin/product/put_product_response.json | 3 + ...oduct_association_collection_response.json | 36 +++ .../get_product_association_response.json | 10 + ...plicated_association_product_response.json | 13 + .../post_product_association_response.json | 10 + ...iation_without_required_data_response.json | 18 ++ .../put_product_association_response.json | 10 + ...collection_with_associations_response.json | 133 +++++---- 30 files changed, 840 insertions(+), 64 deletions(-) create mode 100644 src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php create mode 100644 src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml create mode 100644 tests/Api/Admin/ProductAssociationsTest.php create mode 100644 tests/Api/Responses/Expected/admin/product_association/get_product_association_collection_response.json create mode 100644 tests/Api/Responses/Expected/admin/product_association/get_product_association_response.json create mode 100644 tests/Api/Responses/Expected/admin/product_association/post_duplicated_association_product_response.json create mode 100644 tests/Api/Responses/Expected/admin/product_association/post_product_association_response.json create mode 100644 tests/Api/Responses/Expected/admin/product_association/post_product_association_without_required_data_response.json create mode 100644 tests/Api/Responses/Expected/admin/product_association/put_product_association_response.json diff --git a/features/product/managing_products/adding_associations_to_existing_product.feature b/features/product/managing_products/adding_associations_to_existing_product.feature index 0db83e6146..5caa1d2e62 100644 --- a/features/product/managing_products/adding_associations_to_existing_product.feature +++ b/features/product/managing_products/adding_associations_to_existing_product.feature @@ -9,10 +9,16 @@ Feature: Adding associations to an existing product And the store has "LG G3", "LG headphones" and "LG earphones" products And I am logged in as an administrator - @ui @javascript + @ui @javascript @no-api Scenario: Adding an association to an existing product When I want to modify the "LG G3" product And I associate as "Accessories" the "LG headphones" and "LG earphones" products And I save my changes Then I should be notified that it has been successfully edited And this product should have an association "Accessories" with products "LG headphones" and "LG earphones" + + @api @no-ui + Scenario: Adding an association to na existing product + When I associate as "Accessories" the product "LG G3" with the products "LG headphones" and "LG earphones" + And this product should have an association "Accessories" + And this association should have products "LG headphones" and "LG earphones" diff --git a/features/product/managing_products/adding_product_with_associations.feature b/features/product/managing_products/adding_product_with_associations.feature index 4b11ed192d..3f477b7048 100644 --- a/features/product/managing_products/adding_product_with_associations.feature +++ b/features/product/managing_products/adding_product_with_associations.feature @@ -38,7 +38,7 @@ Feature: Adding a new product with associations And this product should not have an association "Accessories" with product "LG earphones" And the product "LG G3" should appear in the store - @ui @javascript + @ui @javascript @no-api Scenario: Adding a new product with association with numeric code Given the store has 123 product association type When I want to create a new simple product diff --git a/features/product/managing_products/changing_associations_of_product.feature b/features/product/managing_products/changing_associations_of_product.feature index 7a0b6d609f..a618692c4d 100644 --- a/features/product/managing_products/changing_associations_of_product.feature +++ b/features/product/managing_products/changing_associations_of_product.feature @@ -9,7 +9,7 @@ Feature: Changing associations of an existing product And the store has "LG G3", "LG headphones" and "LG earphones" products And I am logged in as an administrator - @ui @javascript + @ui @javascript @no-api Scenario: Changing associated products of a product association Given the product "LG G3" has an association "Accessories" with product "LG headphones" When I want to modify the "LG G3" product @@ -18,7 +18,19 @@ Feature: Changing associations of an existing product Then I should be notified that it has been successfully edited And this product should have an association "Accessories" with products "LG headphones" and "LG earphones" - @ui @javascript + @api @no-ui + Scenario: Adding another product to a product association + Given the product "LG G3" has an association "Accessories" with product "LG headphones" + When I add the product "LG earphones" to this product association + Then this association should have products "LG headphones" and "LG earphones" + + @api @no-ui + Scenario: Changing a product of a product association + Given the product "LG G3" has an association "Accessories" with product "LG headphones" + When I change this product association's product to the "LG earphones" product + Then this association should only have product "LG earphones" + + @ui @javascript @no-api Scenario: Removing an associated product of a product association Given the product "LG G3" has an association "Accessories" with products "LG headphones" and "LG earphones" When I want to modify the "LG G3" product @@ -27,3 +39,9 @@ Feature: Changing associations of an existing product Then I should be notified that it has been successfully edited And this product should have an association "Accessories" with product "LG headphones" And this product should not have an association "Accessories" with product "LG earphones" + + @api @no-ui + Scenario: Removing a product of a product association + Given the product "LG G3" has an association "Accessories" with products "LG headphones" and "LG earphones" + When I remove the product "LG earphones" from this product association + Then this association should only have product "LG headphones" diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php new file mode 100644 index 0000000000..4ab5e0f019 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php @@ -0,0 +1,189 @@ +iAssociateAsTypeTheProductWithTheProducts($type, $owner, [$product]); + } + + /** + * @When /^I (associate as "[^"]+") the (product "[^"]+") with the (products "[^"]+" and "[^"]+")$/ + */ + public function iAssociateAsTypeTheProductWithTheProducts( + ProductAssociationTypeInterface $type, + ProductInterface $owner, + array $products, + ): void { + $associatedProductsData = []; + /** @var ProductInterface $product */ + foreach ($products as $product) { + $associatedProductsData[] = $this->iriConverter->getIriFromItem($product); + } + + $this->client->buildCreateRequest(Resources::PRODUCT_ASSOCIATIONS); + $this->client->addRequestData('type', $this->iriConverter->getIriFromItem($type)); + $this->client->addRequestData('owner', $this->iriConverter->getIriFromItem($owner)); + $this->client->addRequestData('associatedProducts', $associatedProductsData); + $this->client->create(); + + /** @var ProductAssociationInterface $association */ + $association = $this->associationRepository->findOneBy([ + 'owner' => $owner, + 'type' => $type, + ]); + + $this->sharedStorage->set('association', $association); + $this->sharedStorage->set('product', $association->getOwner()); + } + + /** + * @When /^I add the (product "[^"]+") to (this product association)$/ + */ + public function iAddTheProductToThisProductAssociation( + ProductInterface $product, + ProductAssociationInterface $association, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + + $associatedProducts = [$this->iriConverter->getIriFromItem($product)]; + foreach ($association->getAssociatedProducts() as $associatedProduct) { + $associatedProducts[] = $this->iriConverter->getIriFromItem($associatedProduct); + } + + $this->client->setRequestData(['associatedProducts' => $associatedProducts]); + $this->client->update(); + + $this->sharedStorage->set('association', $association); + } + + /** + * @When /^I change (this product association)'s product to the ("[^"]+" product)$/ + */ + public function iChangeThisProductAssociationProductToProduct( + ProductAssociationInterface $association, + ProductInterface $product, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + $this->client->addRequestData('associatedProducts', [$this->iriConverter->getIriFromItem($product)]); + $this->client->update(); + + $this->sharedStorage->set('association', $association); + } + + /** + * @When /^I remove the (product "[^"]+") from (this product association)$/ + */ + public function iRemoveTheProductFromThisProductAssociation( + ProductInterface $product, + ProductAssociationInterface $association, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + + $associatedProducts = []; + foreach ($association->getAssociatedProducts() as $associatedProduct) { + if ($associatedProduct->getCode() !== $product->getCode()) { + $associatedProducts[] = $this->iriConverter->getIriFromItem($associatedProduct); + } + } + + $this->client->setRequestData(['associatedProducts' => $associatedProducts]); + $this->client->update(); + + $this->sharedStorage->set('association', $association); + } + + /** + * @Then /^(this product) should have an (association "[^"]+")$/ + */ + public function thisProductShouldHaveAnAssociation( + ProductInterface $product, + ProductAssociationTypeInterface $type, + ): void { + $response = $this->client->show(Resources::PRODUCTS, $product->getCode()); + $associations = $this->responseChecker->getValue($response, 'associations'); + + $associationTypeIri = $this->sectionAwareIriConverter->getIriFromResourceInSection($type, 'admin'); + + foreach ($associations as $associationIri) { + $response = $this->client->showByIri($associationIri); + $productAssociationType = $this->responseChecker->getValue($response, 'type'); + + if ($associationTypeIri === $productAssociationType) { + return; + } + } + + throw new \InvalidArgumentException(sprintf( + 'Product %s does not have an association of type %s', + $product->getCode(), + $type->getName() + )); + } + + /** + * @Then /^(this association) should only have (product "[^"]+")$/ + */ + public function thisAssociationShouldOnlyHaveProduct( + ProductAssociationInterface $association, + ProductInterface $product, + ): void { + $this->thisAssociationShouldHaveProducts($association, [$product]); + } + + /** + * @Then /^(this association) should have (products "[^"]+" and "[^"]+")$/ + */ + public function thisAssociationShouldHaveProducts( + ProductAssociationInterface $association, + array $products, + ): void { + $response = $this->client->show(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + + $content = $this->responseChecker->getResponseContent($response); + $associatedProducts = $content['associatedProducts']; + + Assert::count($associatedProducts, count($products)); + + /** @var ProductInterface $product */ + foreach ($products as $product) { + Assert::inArray( + $this->sectionAwareIriConverter->getIriFromResourceInSection($product, 'admin'), + $associatedProducts, + ); + } + } +} diff --git a/src/Sylius/Behat/Context/Api/Resources.php b/src/Sylius/Behat/Context/Api/Resources.php index aa7a0e4529..cb28dbe07e 100644 --- a/src/Sylius/Behat/Context/Api/Resources.php +++ b/src/Sylius/Behat/Context/Api/Resources.php @@ -57,6 +57,8 @@ final class Resources public const PRODUCT_ASSOCIATION_TYPES = 'product-association-types'; + public const PRODUCT_ASSOCIATIONS = 'product-associations'; + public const PRODUCT_ATTRIBUTES = 'product-attributes'; public const PRODUCT_IMAGES = 'product-images'; diff --git a/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php b/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php index cf4c6fc3d4..67e35ca6f7 100644 --- a/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php @@ -140,6 +140,8 @@ final class ProductAssociationContext implements Context $product->addAssociation($productAssociation); $this->productAssociationRepository->add($productAssociation); + + $this->sharedStorage->set('product_association', $productAssociation); } private function addProductAssociationTypeTranslation( diff --git a/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php b/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php index b7f91d320e..188c8b9570 100644 --- a/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php @@ -25,6 +25,7 @@ final class ProductAssociationTypeContext implements Context /** * @Transform /^association "([^"]+)"$/ + * @Transform /^associate as "([^"]+)"$/ * @Transform :productAssociationType */ public function getProductAssociationTypeByName($productAssociationTypeName) diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index ab68c9bdf8..bcc518a112 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -103,6 +103,15 @@ + + + + + + + + + diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml index 65db7629b6..17d6d0b772 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml @@ -44,6 +44,7 @@ default: - sylius.behat.context.setup.taxonomy - sylius.behat.context.setup.zone + - sylius.behat.context.api.admin.managing_product_associations - sylius.behat.context.api.admin.managing_product_images - sylius.behat.context.api.admin.managing_products - sylius.behat.context.api.admin.response diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml index 54b1f21de5..906c3ec196 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml @@ -18,9 +18,67 @@ sylius - + + + GET + /admin/product-associations + + sylius.api.product_association_filter + + + + admin:product_association:read + + + + + + POST + /admin/product-associations + + + admin:product_association:create + + + + + admin:product_association:read + + + + + + GET + /admin/product-associations/{id} + + + admin:product_association:read + + + + + + PUT + /admin/product-associations/{id} + + + admin:product_association:update + + + + + admin:product_association:read + + + + + + DELETE + /admin/product-associations/{id} + + GET /shop/product-associations/{id} @@ -34,6 +92,7 @@ + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml index 7234af422e..19c1746a3e 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml @@ -86,6 +86,7 @@ shop:product:read + admin:product:read shop:product:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml index 90affc1d3f..81892c5280 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml @@ -20,9 +20,18 @@ shop:product_association:read + admin:product_association:read + admin:product_association:create shop:product_association:read + + admin:product_association:read + admin:product_association:create + + admin:product_association:read + admin:product_association:create + admin:product_association:update shop:product_association:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml index a609ef5406..7e3bc3de06 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml @@ -33,6 +33,14 @@ + + + partial + partial + + + + exact diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml new file mode 100644 index 0000000000..538cac1f4d --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml index 7bef25b924..b9cad48019 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml @@ -47,6 +47,12 @@ sylius: unique: The option value with given code already exists. value: not_blank: Please enter option value. + association: + unique: An association with this owner and type already exists. + type: + not_blank: Please enter association type. + owner: + not_blank: Please enter association owner. association_type: name: not_blank: Please enter association type name. diff --git a/src/Sylius/Component/Product/Model/ProductAssociation.php b/src/Sylius/Component/Product/Model/ProductAssociation.php index 7821244432..fc51843b7b 100644 --- a/src/Sylius/Component/Product/Model/ProductAssociation.php +++ b/src/Sylius/Component/Product/Model/ProductAssociation.php @@ -64,6 +64,7 @@ class ProductAssociation implements ProductAssociationInterface public function setOwner(?ProductInterface $owner): void { + $owner?->addAssociation($this); $this->owner = $owner; } diff --git a/tests/Api/Admin/ProductAssociationsTest.php b/tests/Api/Admin/ProductAssociationsTest.php new file mode 100644 index 0000000000..adbe51ce9d --- /dev/null +++ b/tests/Api/Admin/ProductAssociationsTest.php @@ -0,0 +1,265 @@ +loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'product/product_with_many_locales.yaml', + ]); + + /** @var ProductAssociationInterface $association */ + $association = $fixtures['product_association']; + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/product-associations/%s', $association->getId()), + server: $this + ->headerBuilder() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association/get_product_association_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_returns_nothing_if_association_not_found(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'product/product_with_many_locales.yaml', + ]); + + $this->client->request( + method: 'GET', + uri: '/api/v2/admin/product-associations/nope', + server: $this + ->headerBuilder() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + ); + + $response = $this->client->getResponse(); + + $this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode()); + } + + /** @test */ + public function it_returns_product_association_collection(): void + { + $this->loadFixturesFromFiles([ + 'product/product_with_many_locales.yaml', + 'authentication/api_administrator.yaml' + ]); + + $this->client->request( + method: 'GET', + uri: '/api/v2/admin/product-associations', + server: $this + ->headerBuilder() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association/get_product_association_collection_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_creates_product_association(): void + { + $this->loadFixturesFromFiles([ + 'product/products_with_associations.yaml', + 'authentication/api_administrator.yaml', + ]); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-associations', + server: $this + ->headerBuilder() + ->withJsonLdContentType() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + content: json_encode([ + 'type' => '/api/v2/admin/product-association-types/similar_products', + 'owner' => '/api/v2/admin/products/CUP', + 'associatedProducts' => [ + '/api/v2/admin/products/MUG', + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association/post_product_association_response', + Response::HTTP_CREATED, + ); + } + + /** @test */ + public function it_updates_product_association(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'product/products_with_associations.yaml', + 'authentication/api_administrator.yaml', + ]); + + /** @var ProductAssociationInterface $association */ + $association = $fixtures['product_association']; + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/product-associations/%s', $association->getId()), + server: $this + ->headerBuilder() + ->withJsonLdContentType() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + content: json_encode([ + 'associatedProducts' => [ + '/api/v2/admin/products/TANKARD', + ], + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association/put_product_association_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_deletes_product_association(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'product/products_with_associations.yaml', + 'authentication/api_administrator.yaml', + ]); + + $header = $this + ->headerBuilder() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + ; + + /** @var ProductAssociationInterface $association */ + $association = $fixtures['product_association']; + $associationId = $association->getId(); + + $this->client->request( + method: 'DELETE', + uri: sprintf('/api/v2/admin/product-associations/%s', $associationId), + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NO_CONTENT); + + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/product-associations/%s', $associationId), + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND); + } + + /** @test */ + public function it_does_not_create_product_association_without_required_data(): void + { + $this->loadFixturesFromFiles([ + 'product/products_with_associations.yaml', + 'authentication/api_administrator.yaml', + ]); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-associations', + server: $this + ->headerBuilder() + ->withJsonLdContentType() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + content: '{}', + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association/post_product_association_without_required_data_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + /** @test */ + public function it_does_not_create_duplicated_product_association(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'product/products_with_associations.yaml', + 'authentication/api_administrator.yaml', + ]); + + /** @var ProductAssociationInterface $association */ + $association = $fixtures['product_association']; + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-associations', + server: $this + ->headerBuilder() + ->withJsonLdContentType() + ->withJsonLdAccept() + ->withAdminUserAuthorization('api@example.com') + ->build() + , + content: json_encode([ + 'type' => sprintf('/api/v2/admin/product-association-types/%s', $association->getType()->getCode()), + 'owner' => sprintf('/api/v2/admin/products/%s', $association->getOwner()->getCode()), + ], \JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association/post_duplicated_association_product_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } +} diff --git a/tests/Api/DataFixtures/ORM/product/product.yaml b/tests/Api/DataFixtures/ORM/product/product.yaml index 063525e051..093931234b 100644 --- a/tests/Api/DataFixtures/ORM/product/product.yaml +++ b/tests/Api/DataFixtures/ORM/product/product.yaml @@ -9,7 +9,7 @@ Sylius\Component\Core\Model\Product: translations: en_US: '@product_translation_mug_en_US' pl_PL: '@product_translation_mug_pl_PL' - attributes: + attributes: - '@product_attribute_value_material_en_US' - '@product_attribute_value_material_pl_PL' product_cap: @@ -95,7 +95,7 @@ Sylius\Component\Product\Model\ProductVariantTranslation: locale: 'en_US' name: 'Red Mug' translatable: '@product_variant_mug_red' - + Sylius\Component\Core\Model\ChannelPricing: channel_pricing_mug_blue_web: channelCode: 'WEB' @@ -114,7 +114,7 @@ Sylius\Component\Core\Model\ChannelPricing: channel_pricing_cap_red_web: channelCode: 'WEB' price: 2000 - + Sylius\Component\Product\Model\ProductOption: product_option_color: code: 'COLOR' @@ -228,3 +228,21 @@ Sylius\Component\Core\Model\OrderItem: order_item: variant: '@product_variant_cap_red' order: '@cart_with_items' + +Sylius\Component\Product\Model\ProductAssociation: + product_association: + type: '@product_association_type' + owner: '@product_mug' + associatedProducts: ['@product_cap'] + +Sylius\Component\Product\Model\ProductAssociationType: + product_association_type: + code: 'similar_products' + translations: + en_US: '@product_association_type_translation' + +Sylius\Component\Product\Model\ProductAssociationTypeTranslation: + product_association_type_translation: + name: 'Similar products' + locale: 'en_US' + translatable: '@product_association_type' diff --git a/tests/Api/DataFixtures/ORM/product/products_with_associations.yaml b/tests/Api/DataFixtures/ORM/product/products_with_associations.yaml index fc716c2e0f..286d57802d 100644 --- a/tests/Api/DataFixtures/ORM/product/products_with_associations.yaml +++ b/tests/Api/DataFixtures/ORM/product/products_with_associations.yaml @@ -33,6 +33,12 @@ Sylius\Component\Core\Model\Product: currentLocale: 'en_US' translations: en_US: '@product_translation_cup_en_US' + product_tankard: + code: 'TANKARD' + channels: [ '@channel_web' ] + currentLocale: 'en_US' + translations: + en_US: '@product_translation_tankard_en_US' product_hat: code: 'HAT' channels: [ '@channel_web' ] @@ -56,6 +62,13 @@ Sylius\Component\Core\Model\ProductTranslation: description: 'Short cup description' shortDescription: 'Cup' translatable: '@product_cup' + product_translation_tankard_en_US: + slug: 'Tankard' + locale: 'en_US' + name: 'Tankard' + description: 'Tankard description' + shortDescription: 'Tankard' + translatable: '@product_tankard' product_translation_hat_en_US: slug: 'Hat' locale: 'en_US' diff --git a/tests/Api/Responses/Expected/admin/product/get_product_response.json b/tests/Api/Responses/Expected/admin/product/get_product_response.json index 220145dfe1..c528d36912 100644 --- a/tests/Api/Responses/Expected/admin/product/get_product_response.json +++ b/tests/Api/Responses/Expected/admin/product/get_product_response.json @@ -24,6 +24,9 @@ "\/api\/v2\/admin\/product-reviews\/@integer@", "\/api\/v2\/admin\/product-reviews\/@integer@" ], + "associations": [ + "\/api\/v2\/admin\/product-associations\/@integer@" + ], "attributes": [ { "@id": "\/api\/v2\/admin\/product-attribute-values\/@integer@", diff --git a/tests/Api/Responses/Expected/admin/product/get_products_response.json b/tests/Api/Responses/Expected/admin/product/get_products_response.json index 11bba4f80e..f52ad67ce7 100644 --- a/tests/Api/Responses/Expected/admin/product/get_products_response.json +++ b/tests/Api/Responses/Expected/admin/product/get_products_response.json @@ -22,6 +22,7 @@ "\/api\/v2\/admin\/channels\/WEB" ], "reviews": [], + "associations": [], "attributes": [], "averageRating": 0, "images": [], @@ -66,6 +67,9 @@ "\/api\/v2\/admin\/product-reviews\/@integer@", "\/api\/v2\/admin\/product-reviews\/@integer@" ], + "associations": [ + "\/api\/v2\/admin\/product-associations\/@integer@" + ], "attributes": [ { "@id": "\/api\/v2\/admin\/product-attribute-values\/@integer@", @@ -139,6 +143,7 @@ "\/api\/v2\/admin\/channels\/WEB" ], "reviews": [], + "associations": [], "attributes": [], "averageRating": 0, "images": [], diff --git a/tests/Api/Responses/Expected/admin/product/post_product_response.json b/tests/Api/Responses/Expected/admin/product/post_product_response.json index 3ceea8e2fd..564f719ce0 100644 --- a/tests/Api/Responses/Expected/admin/product/post_product_response.json +++ b/tests/Api/Responses/Expected/admin/product/post_product_response.json @@ -16,6 +16,7 @@ "\/api\/v2\/admin\/channels\/WEB_GB" ], "reviews": [], + "associations": [], "attributes": [ { "@id": "\/api\/v2\/admin\/product-attribute-values\/@integer@", diff --git a/tests/Api/Responses/Expected/admin/product/put_product_response.json b/tests/Api/Responses/Expected/admin/product/put_product_response.json index 2ad2a9088c..8f69d6e61c 100644 --- a/tests/Api/Responses/Expected/admin/product/put_product_response.json +++ b/tests/Api/Responses/Expected/admin/product/put_product_response.json @@ -24,6 +24,9 @@ "\/api\/v2\/admin\/product-reviews\/@integer@", "\/api\/v2\/admin\/product-reviews\/@integer@" ], + "associations": [ + "\/api\/v2\/admin\/product-associations\/@integer@" + ], "attributes": [ { "@id": "\/api\/v2\/admin\/product-attribute-values\/@integer@", diff --git a/tests/Api/Responses/Expected/admin/product_association/get_product_association_collection_response.json b/tests/Api/Responses/Expected/admin/product_association/get_product_association_collection_response.json new file mode 100644 index 0000000000..1d109c69e2 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_association/get_product_association_collection_response.json @@ -0,0 +1,36 @@ +{ + "@context": "\/api\/v2\/contexts\/ProductAssociation", + "@id": "\/api\/v2\/admin\/product-associations", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "\/api\/v2\/admin\/product-associations\/@integer@", + "@type": "ProductAssociation", + "type": "\/api\/v2\/admin\/product-association-types\/similar_products", + "owner": "\/api\/v2\/admin\/products\/MUG", + "associatedProducts": [ + "\/api\/v2\/admin\/products\/CUP" + ] + } + ], + "hydra:totalItems": 1, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "\/api\/v2\/admin\/product-associations{?owner.code,type.code}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "owner.code", + "property": "owner.code", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "type.code", + "property": "type.code", + "required": false + } + ] + } +} diff --git a/tests/Api/Responses/Expected/admin/product_association/get_product_association_response.json b/tests/Api/Responses/Expected/admin/product_association/get_product_association_response.json new file mode 100644 index 0000000000..f1d45b09fc --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_association/get_product_association_response.json @@ -0,0 +1,10 @@ +{ + "@context": "\/api\/v2\/contexts\/ProductAssociation", + "@id": "\/api\/v2\/admin\/product-associations\/@integer@", + "@type": "ProductAssociation", + "type": "\/api\/v2\/admin\/product-association-types\/similar_products", + "owner": "\/api\/v2\/admin\/products\/MUG", + "associatedProducts": [ + "\/api\/v2\/admin\/products\/CUP" + ] +} diff --git a/tests/Api/Responses/Expected/admin/product_association/post_duplicated_association_product_response.json b/tests/Api/Responses/Expected/admin/product_association/post_duplicated_association_product_response.json new file mode 100644 index 0000000000..0d7e3f3902 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_association/post_duplicated_association_product_response.json @@ -0,0 +1,13 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "owner: An association with this owner and type already exists.", + "violations": [ + { + "propertyPath": "owner", + "message": "An association with this owner and type already exists.", + "code": @string@ + } + ] +} diff --git a/tests/Api/Responses/Expected/admin/product_association/post_product_association_response.json b/tests/Api/Responses/Expected/admin/product_association/post_product_association_response.json new file mode 100644 index 0000000000..aaac4183a3 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_association/post_product_association_response.json @@ -0,0 +1,10 @@ +{ + "@context": "\/api\/v2\/contexts\/ProductAssociation", + "@id": "\/api\/v2\/admin\/product-associations\/@integer@", + "@type": "ProductAssociation", + "type": "\/api\/v2\/admin\/product-association-types\/similar_products", + "owner": "\/api\/v2\/admin\/products\/CUP", + "associatedProducts": [ + "\/api\/v2\/admin\/products\/MUG" + ] +} diff --git a/tests/Api/Responses/Expected/admin/product_association/post_product_association_without_required_data_response.json b/tests/Api/Responses/Expected/admin/product_association/post_product_association_without_required_data_response.json new file mode 100644 index 0000000000..98ea79ebb1 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_association/post_product_association_without_required_data_response.json @@ -0,0 +1,18 @@ +{ + "@context": "\/api\/v2\/contexts\/ConstraintViolationList", + "@type": "ConstraintViolationList", + "hydra:title": "An error occurred", + "hydra:description": "type: Please enter association type.\nowner: Please enter association owner.", + "violations": [ + { + "propertyPath": "type", + "message": "Please enter association type.", + "code": @string@ + }, + { + "propertyPath": "owner", + "message": "Please enter association owner.", + "code": @string@ + } + ] +} diff --git a/tests/Api/Responses/Expected/admin/product_association/put_product_association_response.json b/tests/Api/Responses/Expected/admin/product_association/put_product_association_response.json new file mode 100644 index 0000000000..92dd4f80d8 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_association/put_product_association_response.json @@ -0,0 +1,10 @@ +{ + "@context": "\/api\/v2\/contexts\/ProductAssociation", + "@id": "\/api\/v2\/admin\/product-associations\/@integer@", + "@type": "ProductAssociation", + "type": "\/api\/v2\/admin\/product-association-types\/similar_products", + "owner": "\/api\/v2\/admin\/products\/MUG", + "associatedProducts": [ + "\/api\/v2\/admin\/products\/TANKARD" + ] +} diff --git a/tests/Api/Responses/Expected/shop/product/get_products_collection_with_associations_response.json b/tests/Api/Responses/Expected/shop/product/get_products_collection_with_associations_response.json index 3f8bba1c1b..deb4072eba 100644 --- a/tests/Api/Responses/Expected/shop/product/get_products_collection_with_associations_response.json +++ b/tests/Api/Responses/Expected/shop/product/get_products_collection_with_associations_response.json @@ -51,68 +51,89 @@ "\/api\/v2\/shop\/product-associations\/@integer@" ], "defaultVariant": "\/api\/v2\/shop\/product-variants\/MUG_BLUE" + }, + { + "@id": "\/api\/v2\/shop\/products\/TANKARD", + "@type": "Product", + "productTaxons": [], + "mainTaxon": null, + "reviews": [], + "averageRating": @integer@, + "images": [], + "id": @integer@, + "code": "TANKARD", + "variants": [], + "options": [], + "associations": [], + "createdAt": @date@, + "updatedAt": @date@, + "shortDescription": "Tankard", + "name": "Tankard", + "description": "Tankard description", + "slug": "Tankard", + "defaultVariant": null } ], - "hydra:totalItems": 2, + "hydra:totalItems": 3, "hydra:search": { "@type": "hydra:IriTemplate", "hydra:template": "\/api\/v2\/shop\/products{?translations.name,order[code],order[createdAt],productTaxons.taxon.code,productTaxons.taxon.code[],order[price],order[translation.name],localeCode for order[translation.name],taxon}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ - { - "@type": "IriTemplateMapping", - "variable": "translations.name", - "property": "translations.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[code]", - "property": "code", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[createdAt]", - "property": "createdAt", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "productTaxons.taxon.code", - "property": "productTaxons.taxon.code", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "productTaxons.taxon.code[]", - "property": "productTaxons.taxon.code", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[price]", - "property": "price", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[translation.name]", - "property": "translation", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "localeCode for order[translation.name]", - "property": "localeCode", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "taxon", - "property": null, - "required": false - } - ] + { + "@type": "IriTemplateMapping", + "variable": "translations.name", + "property": "translations.name", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[code]", + "property": "code", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[createdAt]", + "property": "createdAt", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "productTaxons.taxon.code", + "property": "productTaxons.taxon.code", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "productTaxons.taxon.code[]", + "property": "productTaxons.taxon.code", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[price]", + "property": "price", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[translation.name]", + "property": "translation", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "localeCode for order[translation.name]", + "property": "localeCode", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "taxon", + "property": null, + "required": false + } + ] } } From 07a954e6ef0ab26e6fd603628c4e40be27ca176d Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Fri, 15 Dec 2023 13:55:29 +0100 Subject: [PATCH 33/44] Cleanup [API] Unify position sorting --- .../Admin/ManagingProductAssociationsContext.php | 2 +- .../Api/Admin/ManagingProductsContext.php | 1 - .../config/api_resources/ProductAttribute.xml | 3 +++ .../config/api_resources/ProductTaxon.xml | 2 +- .../Resources/config/services/filters.xml | 16 +--------------- .../Product/Model/ProductAssociation.php | 1 - .../get_product_attributes_response.json | 15 ++++++++++++++- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php index 4ab5e0f019..a003b6e9cb 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php @@ -32,7 +32,7 @@ class ManagingProductAssociationsContext implements Context /** * @When /^I (associate as "[^"]+") the (product "[^"]+") with the ("[^"]+" product)$/ */ - public function iAssociateAtTypeTheProductWithTheProduct( + public function iAssociateAsTypeTheProductWithTheProduct( ProductAssociationTypeInterface $type, ProductInterface $owner, ProductInterface $product, diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index aecfca717f..5513a302bd 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -739,7 +739,6 @@ final class ManagingProductsContext implements Context /** * @Then I should see non-translatable attribute :attribute with value :value% - * @Then I should see non-translatable attribute :attribute with value :value % */ public function iShouldSeeNonTranslatableAttributeWithValue(ProductAttributeInterface $attribute, int $value): void { diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml index 7b4254584e..c21ecbf042 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml @@ -26,6 +26,9 @@ GET /admin/product-attributes + + sylius.api.order_filter.position + admin:product_attribute:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml index cae0972ff0..c6578a5538 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml @@ -29,7 +29,7 @@ sylius.api.search_filter.product.code sylius.api.search_filter.taxon.code - sylius.api.product_taxon_order_filter + sylius.api.order_filter.position admin:product_taxon:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml index 7e3bc3de06..d8204511e4 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml @@ -79,14 +79,7 @@ - - - exact - - - - - + @@ -303,13 +296,6 @@ - - - - - - - exact diff --git a/src/Sylius/Component/Product/Model/ProductAssociation.php b/src/Sylius/Component/Product/Model/ProductAssociation.php index fc51843b7b..7821244432 100644 --- a/src/Sylius/Component/Product/Model/ProductAssociation.php +++ b/src/Sylius/Component/Product/Model/ProductAssociation.php @@ -64,7 +64,6 @@ class ProductAssociation implements ProductAssociationInterface public function setOwner(?ProductInterface $owner): void { - $owner?->addAssociation($this); $this->owner = $owner; } diff --git a/tests/Api/Responses/Expected/admin/product_attribute/get_product_attributes_response.json b/tests/Api/Responses/Expected/admin/product_attribute/get_product_attributes_response.json index 2f3b4f9aa8..4bf3c50bd3 100644 --- a/tests/Api/Responses/Expected/admin/product_attribute/get_product_attributes_response.json +++ b/tests/Api/Responses/Expected/admin/product_attribute/get_product_attributes_response.json @@ -184,5 +184,18 @@ "type": "text" } ], - "hydra:totalItems": 9 + "hydra:totalItems": 9, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "\/api\/v2\/admin\/product-attributes{?order[position]}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "order[position]", + "property": "position", + "required": false + } + ] + } } From 882d7359d30f0a8d50a693c024c51f76e7934e68 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Wed, 20 Dec 2023 13:46:05 +0100 Subject: [PATCH 34/44] [API] Cover viewing details of a product --- ..._price_history_from_simple_product.feature | 2 +- ...g_product_edit_page_from_show_page.feature | 4 +- ...g_details_of_product_with_variants.feature | 52 +++++--- .../viewing_details_of_simple_product.feature | 18 +-- ..._non_translatable_attributes_admin.feature | 2 +- ...ct_attributes_in_different_locales.feature | 2 +- .../Api/Admin/ManagingProductsContext.php | 125 +++++++++++++++++- .../Ui/Admin/ProductShowPageContext.php | 26 +++- .../Product/ShowPage/AssociationsElement.php | 8 +- .../ShowPage/AssociationsElementInterface.php | 4 +- .../Product/ShowPage/VariantsElement.php | 14 ++ .../ShowPage/VariantsElementInterface.php | 2 + 12 files changed, 216 insertions(+), 43 deletions(-) diff --git a/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature b/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature index ee51b1e080..97325e1088 100644 --- a/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature +++ b/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature @@ -17,6 +17,6 @@ Feature: Accessing the price history from the simple product show page @ui @no-api Scenario: Being able to access price history from simple product show page Given I am browsing products - When I access "Ursus C-355" product page + When I access the "Ursus C-355" product And I access the price history of a simple product for "United States" channel Then I should see 2 log entries in the catalog price history diff --git a/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature b/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature index d995ddfb03..16c52b3320 100644 --- a/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature +++ b/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature @@ -14,12 +14,12 @@ Feature: Accessing to product edit page from show page @ui @no-api Scenario: Accessing to product edit page from product show page - When I access "Iron shield" product page + When I access the "Iron Shield" product And I go to edit page Then I should be on "Iron shield" product edit page @ui @no-api Scenario: Accessing to variant edit page from product show page - When I access "Iron shield" product page + When I access the "Iron Shield" product And I go to edit page of "Iron shield - very big" variant Then I should be on "Iron shield - very big" variant edit page diff --git a/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature b/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature index 419d259d9c..e8844bf412 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature @@ -18,54 +18,68 @@ Feature: Viewing details of a product with variants And I am browsing products @ui @no-api - Scenario: Viewing a configurable product show page - When I access "Iron Shield" product page + Scenario: Viewing a configurable product + When I access the "Iron Shield" product Then I should see product show page with variants And I should see product name "Iron Shield" - @ui @no-api - Scenario: Viewing taxonomy block + @ui @api + Scenario: Viewing taxonomies Given the store classifies its products as "Shield" and "Equipment" And the product "Iron Shield" has a main taxon "Equipment" And the product "Iron Shield" belongs to taxon "Shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see main taxon is "Equipment" - And I should see product taxon is "Shield" + And I should see product taxon "Shield" - @ui @no-api - Scenario: Viewing options block - When I access "Iron Shield" product page + @ui @api + Scenario: Viewing options + When I access the "Iron Shield" product Then I should see option "Shield size" + @ui @api + Scenario: Viewing variants + When I access the "Iron Shield" product + Then I should see 2 variants + And I should see the "Iron Shield - very big" variant + And I should see the "Iron Shield - very small" variant + @ui @no-api - Scenario: Viewing variants block - When I access "Iron Shield" product page + Scenario: Viewing variants' details + When I access the "Iron Shield" product Then I should see 2 variants And I should see "Iron Shield - very big" variant with code "123456789-xl", priced "$25.00" and current stock 5 and in "United States" channel And I should see "Iron Shield - very small" variant with code "123456789-xs", priced "$15.00" and current stock 12 and in "United States" channel @ui @javascript @api - Scenario: Viewing media block + Scenario: Viewing media Given the "Iron Shield" product has an image "mugs.jpg" with "main" type - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see an image related to this product - @ui @no-api - Scenario: Viewing "more details" block + @ui @api + Scenario: Viewing more details Given the product "Iron Shield" has the slug "iron-shield" And the description of product "Iron Shield" is "Shield created by dwarf" And the meta keywords of product "Iron Shield" is "shield" And the short description of product "Iron Shield" is "good shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product name is "Iron Shield" And I should see product slug is "iron-shield" And I should see product's description is "Shield created by dwarf" And I should see product's meta keywords is "shield" And I should see product's short description is "good shield" - @ui @no-api - Scenario: Viewing associations block + @ui @api + Scenario: Viewing association types Given the store has a "Glass shield" product And the product "Iron Shield" has an association "Similar" with product "Glass shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product + Then I should see product association type "Similar" + + @ui @no-api + Scenario: Viewing associations + Given the store has a "Glass shield" product + And the product "Iron Shield" has an association "Similar" with product "Glass shield" + When I access the "Iron Shield" product Then I should see product association "Similar" with "Glass shield" diff --git a/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature index 1bd0d62e89..d780d17f10 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature @@ -13,7 +13,7 @@ Feature: Viewing details of a simple product @ui @no-api Scenario: Viewing a simple product show page - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product show page without variants And I should see product name "Iron Shield" And I should see product breadcrumb "Iron Shield" @@ -21,14 +21,14 @@ Feature: Viewing details of a simple product @ui @no-api Scenario: Viewing pricing block Given the product "Iron Shield" has original price "$25.00" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see price "$20.00" for channel "United States" And I should see original price "$25.00" for channel "United States" @ui @no-api Scenario: Viewing price block without channel enable Given this product is unavailable in "United States" channel - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product name "Iron Shield" And I should see the product in neither channel And I should not see price for channel "United States" @@ -39,7 +39,7 @@ Feature: Viewing details of a simple product And product's "Iron Shield" code is "123456789" And there are 4 units of product "Iron Shield" available in the inventory And the product "Iron Shield" belongs to "No tax" tax category - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product's code is "123456789" And I should see the product is enabled for channel "United States" And I should see 4 as a current stock of this product @@ -49,7 +49,7 @@ Feature: Viewing details of a simple product Scenario: Viewing taxonomy block Given this product belongs to "Shield" And the product "Iron Shield" has a main taxon "Equipment" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see main taxon is "Equipment" And I should see product taxon is "Shield" @@ -58,7 +58,7 @@ Feature: Viewing details of a simple product Given the store has "Over sized" and "Standard" shipping category And the product "Iron Shield" has height "10.0", width "15.0", depth "20.0", weight "25.0" And this product belongs to "Over sized" shipping category - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product's shipping category is "Over sized" And I should see product's height is 10 And I should see product's width is 15 @@ -68,7 +68,7 @@ Feature: Viewing details of a simple product @ui @javascript @api Scenario: Viewing media block Given the "Iron Shield" product has an image "mugs.jpg" with "main" type - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see an image related to this product @ui @no-api @@ -77,7 +77,7 @@ Feature: Viewing details of a simple product And the description of product "Iron Shield" is "Shield created by dwarf" And the meta keywords of product "Iron Shield" is "shield" And the short description of product "Iron Shield" is "good shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product name is "Iron Shield" And I should see product slug is "iron-shield" And I should see product's description is "Shield created by dwarf" @@ -89,5 +89,5 @@ Feature: Viewing details of a simple product Given the store has "Similar" and "Dwarf equipment" product association types And the store has a "Glass Shield" product And the product "Iron Shield" has an association "Similar" with product "Glass Shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product association "Similar" with "Glass Shield" diff --git a/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature b/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature index b48af9a17c..ebff161863 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature @@ -16,7 +16,7 @@ Feature: Viewing product's non translatable attributes @ui @api Scenario: Viewing product's non translatable attributes along with default ones - When I access "Iron Pickaxe" product page + When I access the "Iron Pickaxe" product Then I should see non-translatable attribute "crit chance" with value 10% And I should see attribute "Material" with value "Iron" in "English (United States)" locale And I should see attribute "Material" with value "Żelazo" in "Polish (Poland)" locale diff --git a/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature b/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature index a7a97098ca..7d00084b0a 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature @@ -17,7 +17,7 @@ Feature: Viewing product's attributes in different locales @ui @api Scenario: Viewing product's attributes defined in different locales - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see attribute "material" with value "oak wood" in "English (United States)" locale And I should see attribute "shield details" with value "oak wood is a very good material." in "English (United States)" locale And I should see attribute "material" with value "drewno dębowe" in "Polish (Poland)" locale diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index 5513a302bd..692f204efa 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -24,8 +24,12 @@ use Sylius\Component\Attribute\Model\AttributeValueInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductTaxonInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Product\Model\ProductAssociationInterface; +use Sylius\Component\Product\Model\ProductAssociationTypeInterface; use Sylius\Component\Product\Model\ProductAttributeInterface; use Sylius\Component\Product\Model\ProductOptionInterface; use Symfony\Component\HttpFoundation\Response; @@ -397,9 +401,9 @@ final class ManagingProductsContext implements Context } /** - * @When I access :product product page + * @When I access the :product product */ - public function iAccessProductPage(ProductInterface $product): void + public function iAccessTheProduct(ProductInterface $product): void { $this->client->show(Resources::PRODUCTS, $product->getCode()); } @@ -430,6 +434,115 @@ final class ManagingProductsContext implements Context $this->sharedStorage->set('response', $this->client->getLastResponse()); } + /** + * @Then I should see main taxon is :taxon + */ + public function iShouldSeeMainTaxonIs(TaxonInterface $taxon): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'mainTaxon'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), + ); + } + + /** + * @Then I should see product taxon :taxon + */ + public function iShouldSeeProductTaxon(TaxonInterface $taxon): void + { + $product = $this->sharedStorage->get('product'); + Assert::isInstanceOf($product, ProductInterface::class); + $productTaxon = $product->getProductTaxons()->filter( + fn(ProductTaxonInterface $productTaxon) => $productTaxon->getTaxon()->getCode() === $taxon->getCode() + )->first(); + Assert::isInstanceOf($productTaxon, ProductTaxonInterface::class); + + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'productTaxons', + $this->sectionAwareIriConverter->getIriFromResourceInSection($productTaxon, 'admin'), + )); + } + + /** + * @Then I should see option :productOption + */ + public function iShouldSeeOption(ProductOptionInterface $productOption): void + { + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'options', + $this->sectionAwareIriConverter->getIriFromResourceInSection($productOption, 'admin'), + )); + } + + /** + * @Then I should see :count variants + */ + public function iShouldSeeVariants(int $count): void + { + Assert::count( + $this->responseChecker->getResponseContent($this->client->getLastResponse())['variants'] ?? [], + $count, + ); + } + + /** + * @Then I should see the :variant variant + */ + public function iShouldSeeTheVariant(ProductVariantInterface $variant): void + { + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'variants', + $this->sectionAwareIriConverter->getIriFromResourceInSection($variant, 'admin'), + )); + } + + /** + * @Then I should see product :field is :value + * @Then I should see product's :field is :value + */ + public function iShouldSeeProductFieldIs(string $field, string $value): void + { + $this->assertResponseHasTranslationFieldWithValue($field, $value); + } + + /** + * @Then I should see product's meta keyword(s) is/are :metaKeywords + */ + public function iShouldSeeProductMetaKeywordsAre(string $metaKeywords): void + { + $this->assertResponseHasTranslationFieldWithValue('metaKeywords', $metaKeywords); + } + + /** + * @Then I should see product's short description is :shortDescription + */ + public function iShouldSeeProductShortDescriptionIs(string $shortDescription): void + { + $this->assertResponseHasTranslationFieldWithValue('shortDescription', $shortDescription); + } + + /** + * @Then I should see product association type :productAssociationType + */ + public function iShouldSeeProductAssociationType(ProductAssociationTypeInterface $productAssociationType): void + { + $associations = $this->responseChecker->getValue($this->client->getLastResponse(), 'associations'); + foreach ($associations as $associationIri) { + /** @var ProductAssociationInterface $association */ + $association = $this->iriConverter->getResourceFromIri($associationIri); + if ($association->getType()->getCode() === $productAssociationType->getCode()) { + return; + } + } + + throw new \InvalidArgumentException( + sprintf('Product association type "%s" not found.', $productAssociationType->getCode()), + ); + } + /** * @Then I should be notified that it has been successfully created */ @@ -985,4 +1098,12 @@ final class ManagingProductsContext implements Context Assert::same((string) $value, $expectedValue); } + + private function assertResponseHasTranslationFieldWithValue(string $field, string $value): void + { + Assert::same( + $this->responseChecker->getTranslationValue($this->client->getLastResponse(), $field), + $value, + ); + } } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php b/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php index 19d0634cde..b228ee9429 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php @@ -60,9 +60,9 @@ final class ProductShowPageContext implements Context } /** - * @When I access :product product page + * @When I access the :product product */ - public function iAccessProductPage(ProductInterface $product): void + public function iAccessTheProduct(ProductInterface $product): void { $this->indexPage->showProductPage($product->getName()); } @@ -285,9 +285,9 @@ final class ProductShowPageContext implements Context } /** - * @Then I should see product taxon is :taxonName + * @Then I should see product taxon :taxonName */ - public function iShouldSeeProductTaxonIs(string $taxonName): void + public function iShouldSeeProductTaxon(string $taxonName): void { Assert::true($this->taxonomyElement->hasProductTaxon($taxonName)); } @@ -385,7 +385,15 @@ final class ProductShowPageContext implements Context */ public function iShouldSeeProductAssociationWith(string $association, string $productName): void { - Assert::true($this->associationsElement->isProductAssociated($association, $productName)); + Assert::true($this->associationsElement->isAssociatedWith($association, $productName)); + } + + /** + * @Then I should see product association type :association + */ + public function iShouldSeeProductAssociationType(string $association): void + { + Assert::true($this->associationsElement->hasAssociation($association)); } /** @@ -423,6 +431,14 @@ final class ProductShowPageContext implements Context )); } + /** + * @Then I should see the :variantName variant + */ + public function iShouldSeeTheVariant(string $variantName): void + { + Assert::true($this->variantsElement->hasProductVariant($variantName)); + } + /** * @Then I should see attribute :attribute with value :value in :nameOfLocale locale */ diff --git a/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElement.php b/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElement.php index e40dc9c410..8677e15bb9 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElement.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElement.php @@ -18,9 +18,13 @@ use FriendsOfBehat\PageObjectExtension\Element\Element; final class AssociationsElement extends Element implements AssociationsElementInterface { - public function isProductAssociated(string $associationName, string $productName): bool + public function hasAssociation(string $associationName): bool + { + return [] !== $this->getAssociatedProducts($this->getElement('associations'), $associationName); + } + + public function isAssociatedWith(string $associationName, string $productName): bool { - /** @var NodeElement $associations */ $associations = $this->getElement('associations'); /** @var NodeElement $product */ diff --git a/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php b/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php index bfd164f1c1..250eaf10ff 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php @@ -15,5 +15,7 @@ namespace Sylius\Behat\Element\Product\ShowPage; interface AssociationsElementInterface { - public function isProductAssociated(string $associationName, string $productName): bool; + public function hasAssociation(string $associationName): bool; + + public function isAssociatedWith(string $associationName, string $productName): bool; } diff --git a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php index b4e15c9434..ef7540695e 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php @@ -26,6 +26,20 @@ final class VariantsElement extends Element implements VariantsElementInterface return \count($variants); } + public function hasProductVariant(string $name): bool + { + $variantRows = $this->getDocument()->findAll('css', '#variants .variants-accordion__title'); + + /** @var NodeElement $variant */ + foreach ($variantRows as $variant) { + if ($variant->find('css', '.content .variant-name')->getText() === $name) { + return true; + } + } + + return false; + } + public function hasProductVariantWithCodePriceAndCurrentStock( string $name, string $code, diff --git a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php index 70c2637024..4d63247e10 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php @@ -17,6 +17,8 @@ interface VariantsElementInterface { public function countVariantsOnPage(): int; + public function hasProductVariant(string $name): bool; + public function hasProductVariantWithCodePriceAndCurrentStock( string $name, string $code, From cfdfe2c61002b7ca373ec8173215def7e0f64f0e Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Wed, 20 Dec 2023 14:58:29 +0100 Subject: [PATCH 35/44] [Behat] Update step names in scenarios after changing name of steps --- ...ccessing_price_history_from_configurable_product.feature | 2 +- ...checking_products_in_the_shop_while_viewing_them.feature | 6 +++--- .../viewing_details_of_simple_product.feature | 2 +- ..._lowest_price_before_discount_for_simple_product.feature | 4 ++-- ...wing_lowest_price_before_discount_within_variant.feature | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature b/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature index a2c432fb02..e924dce57b 100644 --- a/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature +++ b/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature @@ -14,6 +14,6 @@ Feature: Accessing the price history from the configurable product show page @ui @no-api Scenario: Being able to access the price history of variant from the configurable product show page Given I am browsing products - When I access "Wyborowa Vodka" product page + When I access the "Wyborowa Vodka" product And I access the price history of a product variant "Wyborowa Vodka Exquisite" for "United States" channel Then I should see 1 log entries in the catalog price history diff --git a/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature b/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature index adb92da5f9..50874a1713 100644 --- a/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature +++ b/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature @@ -14,18 +14,18 @@ Feature: Checking products in the shop while viewing them @ui @no-api Scenario: Accessing product show page in shop from the product show page where product is available in more than one channel Given this product is available in the "Europe" channel - When I access "Bugatti" product page + When I access the "Bugatti" product And I show this product in the "Europe" channel Then I should see this product in the "Europe" channel in the shop @ui @no-api Scenario: Accessing product show page in shop from the product show page where product is available in one channel - When I access "Bugatti" product page + When I access the "Bugatti" product And I show this product in this channel Then I should see this product in the "Europe" channel in the shop @ui @no-api Scenario: Being unable to access product show page in shop when the product is disabled Given this product has been disabled - When I access "Bugatti" product page + When I access the "Bugatti" product Then I should not be able to show this product in shop diff --git a/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature index d780d17f10..71de4aff4a 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature @@ -51,7 +51,7 @@ Feature: Viewing details of a simple product And the product "Iron Shield" has a main taxon "Equipment" When I access the "Iron Shield" product Then I should see main taxon is "Equipment" - And I should see product taxon is "Shield" + And I should see product taxon "Shield" @ui @no-api Scenario: Viewing shipping block diff --git a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature index ef1421c307..7cfb20af7c 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature @@ -13,10 +13,10 @@ Feature: Seeing the lowest price before the discount for a simple product @ui @no-api Scenario: Seeing price block with lowest price before the discount Given this product's price changed to "$21.00" and original price changed to "$37.00" - When I access "Bizon Z056" product page + When I access the "Bizon Z056" product Then I should see "$42.00" as its lowest price before the discount in "United States" channel @ui @no-api Scenario: Seeing price block without lowest price before the discount - When I access "Bizon Z056" product page + When I access the "Bizon Z056" product Then I should not see the lowest price before the discount in "United States" channel diff --git a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature index c7e7acf1f4..b1e723f55d 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature @@ -15,6 +15,6 @@ Feature: Seeing the lowest price before the discount within variant @ui @no-api Scenario: Seeing price block with lowest price before the discount within variant - When I access "Wyborowa Vodka" product page + When I access the "Wyborowa Vodka" product Then I should not see the lowest price before the discount for "Wyborowa Vodka Rye" variant in "United States" channel And I should see the lowest price before the discount of "$11.00" for "Wyborowa Vodka Potato" variant in "United States" channel From 8cf586d08ac5bd71dc8203fa18b526e651e2791f Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Mon, 18 Dec 2023 15:32:26 +0100 Subject: [PATCH 36/44] Cover handling order shipment --- .../placing_order_with_shipment.feature | 2 +- .../handling_order_shipment/shipping_order.feature | 6 +++--- .../Resources/config/suites/api/order/managing_orders.yml | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature b/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature index 29fd52e4e9..bcb828f2eb 100644 --- a/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature +++ b/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature @@ -15,7 +15,7 @@ Feature: Shipments are in the state "ready" after checkout And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Checking shipment state of a placed order When I view the summary of the order "#00000666" Then it should have shipment in state "Ready" diff --git a/features/order/managing_orders/handling_order_shipment/shipping_order.feature b/features/order/managing_orders/handling_order_shipment/shipping_order.feature index e628d09344..a697dbe5e6 100644 --- a/features/order/managing_orders/handling_order_shipment/shipping_order.feature +++ b/features/order/managing_orders/handling_order_shipment/shipping_order.feature @@ -15,7 +15,7 @@ Feature: Shipping an order And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Finalizing order's shipment Given I view the summary of the order "#00000666" When specify its tracking code as "#00044" @@ -23,13 +23,13 @@ Feature: Shipping an order Then I should be notified that the order has been successfully shipped And it should have shipment in state shipped - @ui + @ui @api Scenario: Unable to finalize shipped order's shipment Given this order has already been shipped When I view the summary of the order "#00000666" Then I should not be able to ship this order - @ui + @ui @api Scenario: Checking the shipment state of a completed order Given this order has already been shipped When I browse orders diff --git a/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml b/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml index aa4bf91ba8..4c0bedb6ae 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml @@ -17,6 +17,7 @@ default: - sylius.behat.context.setup.currency - sylius.behat.context.setup.customer - sylius.behat.context.setup.geographical + - sylius.behat.context.setup.locale - sylius.behat.context.setup.order - sylius.behat.context.setup.payment - sylius.behat.context.setup.product From b090f472c6e8019320d71546042c57d06525dbd5 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Mon, 18 Dec 2023 15:35:24 +0100 Subject: [PATCH 37/44] Cover accessing order from payments and shipments --- ..._payments_order_from_payment_index.feature | 6 ++-- ...hipments_order_from_shipment_index.feature | 6 ++-- .../Api/Admin/ManagingPaymentsContext.php | 31 +++++++++++++++++++ .../Api/Admin/ManagingShipmentsContext.php | 28 +++++++++++++++++ .../Ui/Admin/ManagingPaymentsContext.php | 1 + .../Ui/Admin/ManagingShipmentsContext.php | 1 + .../config/services/contexts/api/admin.xml | 2 ++ 7 files changed, 69 insertions(+), 6 deletions(-) diff --git a/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature b/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature index e05b984b32..4012c30634 100644 --- a/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature +++ b/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature @@ -12,8 +12,8 @@ Feature: Accessing payment's order from the payment index And there is an "#00000001" order with "Apple" product And I am logged in as an administrator - @ui - Scenario: Accessing payment's order from the payments index + @ui @api + Scenario: Accessing payment's order from the payment Given I am browsing payments When I go to the details of the first payment's order - Then I should see order page with details of order "#00000001" + Then I should see the details of order "#00000001" diff --git a/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature b/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature index 62d88582aa..1e3067a39a 100644 --- a/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature +++ b/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature @@ -15,8 +15,8 @@ Feature: Accessing shipment's order from the shipments index And the customer chose "UPS" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui - Scenario: Accessing shipment's order from the shipments index + @ui @api + Scenario: Accessing shipment's order from the shipment When I browse shipments And I move to the details of first shipment's order - Then I should see order page with details of order "00000001" + Then I should see the details of order "#00000001" diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php index e9d7496575..de65e2fc29 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; @@ -23,6 +24,7 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Payment\PaymentTransitions; +use Symfony\Component\HttpFoundation\Request as HttpRequest; use Webmozart\Assert\Assert; final class ManagingPaymentsContext implements Context @@ -31,6 +33,7 @@ final class ManagingPaymentsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SectionAwareIriConverterInterface $sectionAwareIriConverter, + private IriConverterInterface $iriConverter, private string $apiUrlPrefix, ) { } @@ -44,6 +47,34 @@ final class ManagingPaymentsContext implements Context $this->client->index(Resources::PAYMENTS); } + /** + * @When I go to the details of the first payment's order + */ + public function iGoToTheDetailsOfTheFirstPaymentSOrder(): void + { + $firstPayment = $this->responseChecker->getCollection($this->client->getLastResponse())[0]; + + /** @var OrderInterface $order */ + $order = $this->iriConverter->getResourceFromIri($firstPayment['order']); + + $this->client->customItemAction(Resources::ORDERS, $order->getTokenValue(), HttpRequest::METHOD_GET, 'payments'); + } + + /** + * @Then I should see the details of order :order + */ + public function iShouldSeeOrderWithDetails(OrderInterface $order): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'order', + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), + ), + sprintf('Order with number %s does not exist', $order->getNumber()), + ); + } + /** * @When I complete the payment of order :order */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php index 41028bb7e1..d5a948bb80 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php @@ -59,6 +59,19 @@ final class ManagingShipmentsContext implements Context $this->client->addFilter('state', $state); } + /** + * @When I move to the details of first shipment's order + */ + public function iMoveToDetailsOfFirstShipment(): void + { + $firstShipment = $this->responseChecker->getCollection($this->client->getLastResponse())[0]; + + /** @var OrderInterface $order */ + $order = $this->iriConverter->getResourceFromIri($firstShipment['order']); + + $this->client->customItemAction(Resources::ORDERS, $order->getTokenValue(), HttpRequest::METHOD_GET, 'shipments'); + } + /** * @When I choose :channel as a channel filter */ @@ -290,6 +303,21 @@ final class ManagingShipmentsContext implements Context Assert::same($productUnitsCounter, $amount); } + /** + * @Then I should see the details of order :order + */ + public function iShouldSeeOrderWithDetails(OrderInterface $order): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'order', + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), + ), + sprintf('Order with number %s does not exist', $order->getNumber()), + ); + } + private function isShipmentForOrder(OrderInterface $order): bool { return $this->responseChecker->hasItemWithValue( diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php index 939991e430..4266942e52 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php @@ -108,6 +108,7 @@ final class ManagingPaymentsContext implements Context /** * @Then I should see order page with details of order :order + * @Then I should see the details of order :order */ public function iShouldSeeOrderPageWithDetailsOfOrder(OrderInterface $order): void { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php index 425c87cf12..c5a3d71a70 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php @@ -172,6 +172,7 @@ final class ManagingShipmentsContext implements Context /** * @Then I should see order page with details of order :order + * @Then I should see the details of order :order */ public function iShouldSeeOrderPageWithDetailsOfOrder(OrderInterface $order): void { diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index bcc518a112..bc70255742 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -206,6 +206,7 @@ + %sylius.security.new_api_route% @@ -221,6 +222,7 @@ + From d6b36dafc91d8c72c9de3d71e09b0eb1a5c35185 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Mon, 18 Dec 2023 15:37:47 +0100 Subject: [PATCH 38/44] Cover preventing to view customers cart from the orders resource --- .../being_unable_to_see_cart.feature | 4 +- .../Api/Admin/ManagingOrdersContext.php | 98 +++++++++++++++ .../Behat/Context/Setup/CartContext.php | 1 + .../QueryExtension/OrderExtension.php | 71 +++++++++++ .../Resources/config/services/extensions.xml | 9 ++ .../QueryExtension/OrderExtensionSpec.php | 118 ++++++++++++++++++ tests/Api/Admin/OrdersTest.php | 4 +- .../ORM/order/fulfilled_order.yaml | 1 + .../gets_orders_for_customer_response.json | 4 +- 9 files changed, 304 insertions(+), 6 deletions(-) create mode 100644 src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php create mode 100644 src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php diff --git a/features/order/managing_orders/order_details/being_unable_to_see_cart.feature b/features/order/managing_orders/order_details/being_unable_to_see_cart.feature index 92e846a0dd..a88a25b0a8 100644 --- a/features/order/managing_orders/order_details/being_unable_to_see_cart.feature +++ b/features/order/managing_orders/order_details/being_unable_to_see_cart.feature @@ -10,7 +10,7 @@ Feature: Being unable to see details of a cart And the customer added "PHP T-Shirt" product to the cart And I am logged in as an administrator - @ui - Scenario: Seeing basic information about an order + @ui @api + Scenario: Being unable to see the details of a cart When I try to view the summary of the customer's latest cart Then I should be informed that the order does not exist diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php index 92bf3eacd9..1ce4b1c591 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php @@ -16,6 +16,7 @@ namespace Sylius\Behat\Context\Api\Admin; use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; +use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Context\Api\Subresources; @@ -29,6 +30,7 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; +use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Model\ShippingMethodInterface; use Sylius\Component\Currency\Model\CurrencyInterface; use Sylius\Component\Order\OrderTransitions; @@ -43,6 +45,7 @@ final class ManagingOrdersContext implements Context public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private RequestFactoryInterface $requestFactory, private IriConverterInterface $iriConverter, private SecurityServiceInterface $adminSecurityService, private SharedStorageInterface $sharedStorage, @@ -104,6 +107,30 @@ final class ManagingOrdersContext implements Context $this->client->addFilter('checkoutCompletedAt[after]', $dateTime); } + /** + * @When specify its tracking code as :trackingCode + */ + public function specifyItsTrackingCodeAs(string $trackingCode): void + { + $shipment = $this->sharedStorage->get('order')->getShipments()->first(); + + $this->client->buildUpdateRequest( + Resources::SHIPMENTS, + (string) $shipment->getId(), + ); + + $this->client->addRequestData('tracking', $trackingCode); + $this->client->update(); + } + + /** + * @When /^I try to view the summary of the (customer's latest cart)$/ + */ + public function iTryToViewTheSummaryOfTheCustomersLatestCart(OrderInterface $cart): void + { + $this->client->show(Resources::ORDERS, $cart->getTokenValue()); + } + /** * @When I specify filter date to as :dateTime */ @@ -989,6 +1016,77 @@ final class ManagingOrdersContext implements Context Assert::same($this->getTotalAsInt($subTotal), $orderItem['unitPrice'] * $orderItem['quantity'] + $unitPromotionAdjustments); } + /** + * @Then I should be notified that the order has been successfully shipped + */ + public function iShouldBeNotifiedThatTheOrderHasBeenSuccessfullyShipped(): void + { + $response = $this->client->getLastResponse(); + Assert::true( + $this->responseChecker->isUpdateSuccessful($response), + 'Order could not be shipped. Reason: ' . $response->getContent(), + ); + } + + /** + * @Then it should have shipment in state shipped + */ + public function itShouldHaveShipmentInStateShipped(): void + { + $shipmentIri = $this->responseChecker->getValue( + $this->client->show(Resources::ORDERS, $this->sharedStorage->get('order')->getTokenValue()), + 'shipments', + )[0]; + + Assert::true( + $this->responseChecker->hasValue($this->client->showByIri($shipmentIri['@id']), 'state', ShipmentInterface::STATE_SHIPPED), + sprintf('Shipment for this order is not %s', ShipmentInterface::STATE_SHIPPED), + ); + } + + /** + * @Then this order should have order shipping state :orderShippingState + */ + public function thisOrderShouldHaveOrderShippingState(string $orderShippingState): void + { + $ordersResponse = $this->client->index(Resources::ORDERS); + + Assert::true( + $this->responseChecker->hasItemWithValue($ordersResponse, 'shippingState', strtolower($orderShippingState)), + sprintf('Order does not have %s shipping state', $orderShippingState), + ); + } + + /** + * @Then I should not be able to ship this order + */ + public function iShouldNotBeAbleToShipThisOrder(): void + { + $order = $this->sharedStorage->get('order'); + + $this->client->applyTransition( + Resources::SHIPMENTS, + (string) $order->getShipments()->first()->getId(), + ShipmentTransitions::TRANSITION_SHIP, + ); + + Assert::false( + $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), + 'Order has been shipped, but should not.', + ); + } + + /** + * @Then I should be informed that the order does not exist + */ + public function iShouldBeInformedThatTheOrderDoesNotExist(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Not Found', + ); + } + /** * @Then there should be :count shipping address changes in the registry */ diff --git a/src/Sylius/Behat/Context/Setup/CartContext.php b/src/Sylius/Behat/Context/Setup/CartContext.php index c87952f061..81f7bdfd8e 100644 --- a/src/Sylius/Behat/Context/Setup/CartContext.php +++ b/src/Sylius/Behat/Context/Setup/CartContext.php @@ -66,6 +66,7 @@ final class CartContext implements Context * @Given /^I (?:have|had) (product "[^"]+") in the (cart)$/ * @Given /^I have (product "[^"]+") added to the (cart)$/ * @Given /^the (?:customer|visitor) has (product "[^"]+") in the (cart)$/ + * @Given /^the (?:customer|visitor) added ("[^"]+" product) to the (cart)$/ * @When /^the (?:customer|visitor) try to add (product "[^"]+") in the customer (cart)$/ */ public function iAddedProductToTheCart(ProductInterface $product, ?string $tokenValue): void diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php new file mode 100644 index 0000000000..f51d3b9e57 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php @@ -0,0 +1,71 @@ + $orderStatesToFilterOut + */ + public function __construct( + private UserContextInterface $userContext, + private array $orderStatesToFilterOut, + ) { + } + + /** + * @param array $context + */ + public function applyToCollection( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + string $operationName = null, + array $context = [], + ): void { + if (!is_a($resourceClass, OrderInterface::class, true)) { + return; + } + + $user = $this->userContext->getUser(); + if ($user instanceof AdminUserInterface && in_array('ROLE_API_ACCESS', $user->getRoles(), true)) { + + $stateParameter = $queryNameGenerator->generateParameterName('state'); + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder + ->andWhere(sprintf('%s.state != :%s', $rootAlias, $stateParameter)) + ->setParameter($stateParameter, $this->orderStatesToFilterOut) + ; + } + } + + /** + * @param array $context + * @param array $identifiers + */ + public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void + { + $this->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml index 22a4d7df2e..962c0fd0af 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml @@ -129,6 +129,15 @@ + + + + Sylius\Component\Order\Model\OrderInterface::STATE_CART + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php new file mode 100644 index 0000000000..16cee480eb --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php @@ -0,0 +1,118 @@ +beConstructedWith($userContext, ['cart']); + } + + function it_does_not_apply_conditions_to_collection_for_shop_user( + UserContextInterface $userContext, + QueryBuilder $queryBuilder, + ShopUserInterface $shopUser, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->willReturn($shopUser); + + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToCollection( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + Request::METHOD_GET, + [], + ); + } + + function it_does_not_apply_conditions_to_item_for_shop_user( + UserContextInterface $userContext, + QueryBuilder $queryBuilder, + ShopUserInterface $shopUser, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->willReturn($shopUser); + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + [], + Request::METHOD_GET, + [], + ); + } + + function it_applies_conditions_to_collection_for_admin( + UserContextInterface $userContext, + AdminUserInterface $adminUser, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->willReturn($adminUser); + $adminUser->getRoles()->willReturn(['ROLE_API_ACCESS']); + + $queryBuilder->getRootAliases()->willReturn(['o']); + + $queryNameGenerator->generateParameterName('state')->shouldBeCalled()->willReturn('state'); + $queryBuilder->andWhere('o.state != :state')->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->setParameter('state', ['cart'])->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + + $this->applyToCollection( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + Request::METHOD_GET, + ); + } + + function it_applies_conditions_to_item_for_admin( + UserContextInterface $userContext, + AdminUserInterface $adminUser, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->willReturn(['o']); + + $userContext->getUser()->willReturn($adminUser); + $adminUser->getRoles()->willReturn(['ROLE_API_ACCESS']); + + $queryBuilder->getRootAliases()->willReturn(['o']); + + $queryNameGenerator->generateParameterName('state')->shouldBeCalled()->willReturn('state'); + $queryBuilder->andWhere('o.state != :state')->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->setParameter('state', ['cart'])->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + [], + Request::METHOD_GET, + ); + } +} diff --git a/tests/Api/Admin/OrdersTest.php b/tests/Api/Admin/OrdersTest.php index 689faca3ba..7c95a49a9c 100644 --- a/tests/Api/Admin/OrdersTest.php +++ b/tests/Api/Admin/OrdersTest.php @@ -72,8 +72,8 @@ final class OrdersTest extends JsonApiTestCase $fixtures = $this->loadFixturesFromFiles([ 'authentication/api_administrator.yaml', 'channel.yaml', - 'customer.yaml', - 'customer_order.yaml', + 'order/customer.yaml', + 'order/fulfilled_order.yaml', ]); /** @var CustomerInterface $customer */ diff --git a/tests/Api/DataFixtures/ORM/order/fulfilled_order.yaml b/tests/Api/DataFixtures/ORM/order/fulfilled_order.yaml index 7d18c8383f..7e2bf69d8a 100644 --- a/tests/Api/DataFixtures/ORM/order/fulfilled_order.yaml +++ b/tests/Api/DataFixtures/ORM/order/fulfilled_order.yaml @@ -4,6 +4,7 @@ Sylius\Component\Core\Model\Order: currencyCode: "USD" localeCode: "en_US" state: "fulfilled" + checkoutState: "fulfilled" tokenValue: "token" customer: "@customer_tony" billingAddress: "@billing_address" diff --git a/tests/Api/Responses/Expected/admin/order/gets_orders_for_customer_response.json b/tests/Api/Responses/Expected/admin/order/gets_orders_for_customer_response.json index 5405ec6af0..b596b5a87b 100644 --- a/tests/Api/Responses/Expected/admin/order/gets_orders_for_customer_response.json +++ b/tests/Api/Responses/Expected/admin/order/gets_orders_for_customer_response.json @@ -38,7 +38,7 @@ "shipments": [], "currencyCode": "USD", "localeCode": "en_US", - "checkoutState": "cart", + "checkoutState": "fulfilled", "paymentState": "cart", "shippingState": "cart", "tokenValue": "token", @@ -46,7 +46,7 @@ "items": [], "itemsTotal": 0, "total": 0, - "state": "new", + "state": "fulfilled", "itemsSubtotal": 0, "taxTotal": 0, "shippingTaxTotal": 0, From 40676795b36d1910a0e45a12ac726bb12c74349f Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 19 Dec 2023 09:32:59 +0100 Subject: [PATCH 39/44] Remove no more valid scenarios --- ...s_only_for_correctly_logged_in_users.feature | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature b/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature index 5b313e8daf..383294f4b2 100644 --- a/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature +++ b/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature @@ -172,20 +172,3 @@ Feature: Allowing access only for correctly logged in users And the customer has product "Stark T-Shirt" in the cart And the customer logged out Then the visitor has no access to change product "Stark T-Shirt" quantity to 2 in the customer cart - - @api - Scenario: Accessing to the customers cart by the admin - Given the customer logged in - And the customer has product "Stark T-Shirt" in the cart - And the customer logged out - And there is logged in the administrator - When the administrator try to see the summary of customer's cart - Then the administrator should see "Stark T-Shirt" product with quantity 1 in the customer cart - - @api - Scenario: Accessing to the visitors cart by the admin - Given there is the visitor - And the visitor has product "Stark T-Shirt" in the cart - And there is logged in the administrator - When the administrator try to see the summary of customer's cart - Then the administrator should see "Stark T-Shirt" product with quantity 1 in the visitor cart From b90c7872ee9b44bd712eea63415250f34f4737c3 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Tue, 19 Dec 2023 13:34:56 +0100 Subject: [PATCH 40/44] Use SectionProvider in OrderExtension --- .../QueryExtension/OrderExtension.php | 7 ++-- .../Resources/config/services/extensions.xml | 2 +- .../QueryExtension/OrderExtensionSpec.php | 40 +++++++++---------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php index f51d3b9e57..b230fd98fa 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php @@ -18,6 +18,8 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; +use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; +use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\OrderInterface; @@ -28,7 +30,7 @@ final class OrderExtension implements ContextAwareQueryCollectionExtensionInterf * @param array $orderStatesToFilterOut */ public function __construct( - private UserContextInterface $userContext, + private SectionProviderInterface $sectionProvider, private array $orderStatesToFilterOut, ) { } @@ -47,8 +49,7 @@ final class OrderExtension implements ContextAwareQueryCollectionExtensionInterf return; } - $user = $this->userContext->getUser(); - if ($user instanceof AdminUserInterface && in_array('ROLE_API_ACCESS', $user->getRoles(), true)) { + if ($this->sectionProvider->getSection() instanceof AdminApiSection) { $stateParameter = $queryNameGenerator->generateParameterName('state'); $rootAlias = $queryBuilder->getRootAliases()[0]; diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml index 962c0fd0af..e25dc723a7 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml @@ -130,7 +130,7 @@ - + Sylius\Component\Order\Model\OrderInterface::STATE_CART diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php index 16cee480eb..834ee0d72b 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php @@ -17,25 +17,26 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; -use Sylius\Component\Core\Model\AdminUserInterface; +use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; +use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection; +use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; use Sylius\Component\Core\Model\OrderInterface; -use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\HttpFoundation\Request; final class OrderExtensionSpec extends ObjectBehavior { - function let(UserContextInterface $userContext): void + function let(SectionProviderInterface $sectionProvider): void { - $this->beConstructedWith($userContext, ['cart']); + $this->beConstructedWith($sectionProvider, ['cart']); } - function it_does_not_apply_conditions_to_collection_for_shop_user( - UserContextInterface $userContext, + function it_does_not_apply_conditions_to_collection_for_shop( QueryBuilder $queryBuilder, - ShopUserInterface $shopUser, + SectionProviderInterface $sectionProvider, + ShopApiSection $shopApiSection, QueryNameGeneratorInterface $queryNameGenerator, ): void { - $userContext->getUser()->willReturn($shopUser); + $sectionProvider->getSection()->willReturn($shopApiSection); $queryBuilder->getRootAliases()->shouldNotBeCalled(); @@ -48,13 +49,14 @@ final class OrderExtensionSpec extends ObjectBehavior ); } - function it_does_not_apply_conditions_to_item_for_shop_user( - UserContextInterface $userContext, + function it_does_not_apply_conditions_to_item_for_shop( QueryBuilder $queryBuilder, - ShopUserInterface $shopUser, + SectionProviderInterface $sectionProvider, + ShopApiSection $shopApiSection, QueryNameGeneratorInterface $queryNameGenerator, ): void { - $userContext->getUser()->willReturn($shopUser); + $sectionProvider->getSection()->willReturn($shopApiSection); + $queryBuilder->getRootAliases()->shouldNotBeCalled(); $this->applyToItem( @@ -68,13 +70,12 @@ final class OrderExtensionSpec extends ObjectBehavior } function it_applies_conditions_to_collection_for_admin( - UserContextInterface $userContext, - AdminUserInterface $adminUser, + AdminApiSection $adminApiSection, + SectionProviderInterface $sectionProvider, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ): void { - $userContext->getUser()->willReturn($adminUser); - $adminUser->getRoles()->willReturn(['ROLE_API_ACCESS']); + $sectionProvider->getSection()->willReturn($adminApiSection); $queryBuilder->getRootAliases()->willReturn(['o']); @@ -91,15 +92,14 @@ final class OrderExtensionSpec extends ObjectBehavior } function it_applies_conditions_to_item_for_admin( - UserContextInterface $userContext, - AdminUserInterface $adminUser, + AdminApiSection $adminApiSection, + SectionProviderInterface $sectionProvider, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ): void { $queryBuilder->getRootAliases()->willReturn(['o']); - $userContext->getUser()->willReturn($adminUser); - $adminUser->getRoles()->willReturn(['ROLE_API_ACCESS']); + $sectionProvider->getSection()->willReturn($adminApiSection); $queryBuilder->getRootAliases()->willReturn(['o']); From 6b57260f9a5c93ca0f8d6c4fca114a950a35fc0b Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Wed, 20 Dec 2023 12:01:24 +0100 Subject: [PATCH 41/44] Move applyToCollection logic to private method and use in both cases --- .../QueryExtension/OrderExtension.php | 51 ++++++++++++------- .../QueryExtension/OrderExtensionSpec.php | 1 - 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php index b230fd98fa..4034b68a4b 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php @@ -17,10 +17,8 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionEx use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; -use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; -use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\OrderInterface; /** @experimental */ @@ -45,28 +43,43 @@ final class OrderExtension implements ContextAwareQueryCollectionExtensionInterf string $operationName = null, array $context = [], ): void { - if (!is_a($resourceClass, OrderInterface::class, true)) { - return; - } - - if ($this->sectionProvider->getSection() instanceof AdminApiSection) { - - $stateParameter = $queryNameGenerator->generateParameterName('state'); - $rootAlias = $queryBuilder->getRootAliases()[0]; - - $queryBuilder - ->andWhere(sprintf('%s.state != :%s', $rootAlias, $stateParameter)) - ->setParameter($stateParameter, $this->orderStatesToFilterOut) - ; - } + $this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass); } /** * @param array $context * @param array $identifiers */ - public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void - { - $this->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + public function applyToItem( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + array $identifiers, + string $operationName = null, + array $context = [] + ): void { + $this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass); + } + + private function filterOutOrders( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ): void { + if (!is_a($resourceClass, OrderInterface::class, true)) { + return; + } + + if (!$this->sectionProvider->getSection() instanceof AdminApiSection) { + return; + } + + $stateParameter = $queryNameGenerator->generateParameterName('state'); + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder + ->andWhere(sprintf('%s.state != :%s', $rootAlias, $stateParameter)) + ->setParameter($stateParameter, $this->orderStatesToFilterOut) + ; } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php index 834ee0d72b..b86fa2b9c5 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php @@ -16,7 +16,6 @@ namespace spec\Sylius\Bundle\ApiBundle\Doctrine\QueryExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use PhpSpec\ObjectBehavior; -use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection; use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; From 01884e28a503b2043cb5ffbb938ad9657e48ff44 Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Wed, 20 Dec 2023 12:03:34 +0100 Subject: [PATCH 42/44] Rename fulfilled_order file to fulfilled --- tests/Api/Admin/CustomersTest.php | 2 +- tests/Api/Admin/OrdersTest.php | 2 +- .../ORM/order/{fulfilled_order.yaml => fulfilled.yaml} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/Api/DataFixtures/ORM/order/{fulfilled_order.yaml => fulfilled.yaml} (100%) diff --git a/tests/Api/Admin/CustomersTest.php b/tests/Api/Admin/CustomersTest.php index 8774f2eb49..cc9e55f0e3 100644 --- a/tests/Api/Admin/CustomersTest.php +++ b/tests/Api/Admin/CustomersTest.php @@ -71,7 +71,7 @@ final class CustomersTest extends JsonApiTestCase 'authentication/api_administrator.yaml', 'customer.yaml', 'channel.yaml', - 'order/fulfilled_order.yaml', + 'order/fulfilled.yaml', ]); $header = $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(); diff --git a/tests/Api/Admin/OrdersTest.php b/tests/Api/Admin/OrdersTest.php index 7c95a49a9c..2404a6c32f 100644 --- a/tests/Api/Admin/OrdersTest.php +++ b/tests/Api/Admin/OrdersTest.php @@ -73,7 +73,7 @@ final class OrdersTest extends JsonApiTestCase 'authentication/api_administrator.yaml', 'channel.yaml', 'order/customer.yaml', - 'order/fulfilled_order.yaml', + 'order/fulfilled.yaml', ]); /** @var CustomerInterface $customer */ diff --git a/tests/Api/DataFixtures/ORM/order/fulfilled_order.yaml b/tests/Api/DataFixtures/ORM/order/fulfilled.yaml similarity index 100% rename from tests/Api/DataFixtures/ORM/order/fulfilled_order.yaml rename to tests/Api/DataFixtures/ORM/order/fulfilled.yaml From db8a38ff40b9e50ceb95c3e1df85e5aee32f6daf Mon Sep 17 00:00:00 2001 From: Kamil Grygierzec Date: Wed, 20 Dec 2023 15:55:38 +0100 Subject: [PATCH 43/44] Add parameter for order state to filter out and fix the OrdrerExtension logic --- .../DependencyInjection/Configuration.php | 5 +++++ .../SyliusApiExtension.php | 1 + .../QueryExtension/OrderExtension.php | 7 ++++--- .../Resources/config/app/config.yaml | 2 ++ .../Resources/config/services/extensions.xml | 4 +--- .../SyliusApiExtensionTest.php | 19 ++++++++++++++++++ .../QueryExtension/OrderExtensionSpec.php | 20 +++++++++++++++---- 7 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php index 2fe8e17ceb..07e6c289d7 100644 --- a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php @@ -36,6 +36,11 @@ final class Configuration implements ConfigurationInterface ->defaultFalse() ->end() ->end() + ->children() + ->arrayNode('order_states_to_filter_out') + ->scalarPrototype()->end() + ->end() + ->end() ->children() ->variableNode('product_image_prefix') ->defaultValue('media/image') diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php index 0091094ec5..2bc40dab2e 100644 --- a/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php @@ -38,6 +38,7 @@ final class SyliusApiExtension extends Extension implements PrependExtensionInte 'sylius_api.filter_eager_loading_extension.restricted_resources', $config['filter_eager_loading_extension']['restricted_resources'], ); + $container->setParameter('sylius_api.order_states_to_filter_out', $config['order_states_to_filter_out']); $loader->load('services.xml'); diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php index 4034b68a4b..2343bf3c57 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php @@ -16,6 +16,7 @@ namespace Sylius\Bundle\ApiBundle\Doctrine\QueryExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; @@ -56,7 +57,7 @@ final class OrderExtension implements ContextAwareQueryCollectionExtensionInterf string $resourceClass, array $identifiers, string $operationName = null, - array $context = [] + array $context = [], ): void { $this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass); } @@ -78,8 +79,8 @@ final class OrderExtension implements ContextAwareQueryCollectionExtensionInterf $rootAlias = $queryBuilder->getRootAliases()[0]; $queryBuilder - ->andWhere(sprintf('%s.state != :%s', $rootAlias, $stateParameter)) - ->setParameter($stateParameter, $this->orderStatesToFilterOut) + ->andWhere($queryBuilder->expr()->notIn(sprintf('%s.state', $rootAlias), sprintf(':%s', $stateParameter))) + ->setParameter($stateParameter, $this->orderStatesToFilterOut, ArrayParameterType::STRING) ; } } diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml index 361c073aa3..4ee15c0e14 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml @@ -32,6 +32,8 @@ sylius_api: '%sylius.model.product.class%': operations: shop_get: ~ + order_states_to_filter_out: + - !php/const Sylius\Component\Core\Model\OrderInterface::STATE_CART api_platform: patch_formats: diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml index e25dc723a7..e46f384afd 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml @@ -131,9 +131,7 @@ - - Sylius\Component\Order\Model\OrderInterface::STATE_CART - + %sylius_api.order_states_to_filter_out% diff --git a/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php index 14d823883d..f9de62d6bf 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php @@ -21,6 +21,7 @@ use Sylius\Bundle\ApiBundle\DependencyInjection\SyliusApiExtension; use Sylius\Bundle\ApiBundle\Tests\Stub\CommandDataTransformerStub; use Sylius\Bundle\ApiBundle\Tests\Stub\DocumentationModifierStub; use Sylius\Bundle\ApiBundle\Tests\Stub\PaymentConfigurationProviderStub; +use Sylius\Component\Core\Model\OrderInterface; use Symfony\Component\DependencyInjection\Definition; final class SyliusApiExtensionTest extends AbstractExtensionTestCase @@ -87,6 +88,24 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase ); } + /** @test */ + public function it_loads_order_states_to_filter_out_parameter_properly(): void + { + $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); + + $this->load([ + 'order_states_to_filter_out' => [ + OrderInterface::STATE_CART, + OrderInterface::STATE_NEW, + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius_api.order_states_to_filter_out', + [OrderInterface::STATE_CART, OrderInterface::STATE_NEW], + ); + } + /** @test */ public function it_loads_default_filter_eager_loading_extension_restricted_operations_configuration_properly(): void { diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php index b86fa2b9c5..0c936fed9b 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Doctrine\QueryExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\ORM\Query\Expr; use Doctrine\ORM\QueryBuilder; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; @@ -73,14 +75,19 @@ final class OrderExtensionSpec extends ObjectBehavior SectionProviderInterface $sectionProvider, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, + Expr $expr, + Expr\Func $exprNotIn, ): void { $sectionProvider->getSection()->willReturn($adminApiSection); $queryBuilder->getRootAliases()->willReturn(['o']); $queryNameGenerator->generateParameterName('state')->shouldBeCalled()->willReturn('state'); - $queryBuilder->andWhere('o.state != :state')->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); - $queryBuilder->setParameter('state', ['cart'])->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + + $queryBuilder->expr()->willReturn($expr); + $expr->notIn('o.state', ':state')->willReturn($exprNotIn); + $queryBuilder->andWhere($exprNotIn)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->setParameter('state', ['cart'], ArrayParameterType::STRING)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); $this->applyToCollection( $queryBuilder, @@ -95,6 +102,8 @@ final class OrderExtensionSpec extends ObjectBehavior SectionProviderInterface $sectionProvider, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, + Expr $expr, + Expr\Func $exprNotIn, ): void { $queryBuilder->getRootAliases()->willReturn(['o']); @@ -103,8 +112,11 @@ final class OrderExtensionSpec extends ObjectBehavior $queryBuilder->getRootAliases()->willReturn(['o']); $queryNameGenerator->generateParameterName('state')->shouldBeCalled()->willReturn('state'); - $queryBuilder->andWhere('o.state != :state')->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); - $queryBuilder->setParameter('state', ['cart'])->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + + $queryBuilder->expr()->willReturn($expr); + $expr->notIn('o.state', ':state')->willReturn($exprNotIn); + $queryBuilder->andWhere($exprNotIn)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->setParameter('state', ['cart'], ArrayParameterType::STRING)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); $this->applyToItem( $queryBuilder, From 5e714820e84301aec6e26f039e9d00e2edd37eb9 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Thu, 21 Dec 2023 18:14:45 +0100 Subject: [PATCH 44/44] Replace the DateTimeProvider with the Clock --- src/Sylius/Behat/Context/Setup/OrderContext.php | 8 ++++---- .../Behat/Resources/config/services/contexts/setup.xml | 2 +- .../api/product/viewing_product_in_admin_panel.yaml | 2 +- .../Behat/Resources/config/suites/ui/admin/dashboard.yaml | 2 +- .../suites/ui/product/viewing_product_in_admin_panel.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Sylius/Behat/Context/Setup/OrderContext.php b/src/Sylius/Behat/Context/Setup/OrderContext.php index 79967e7f00..3d5c11db78 100644 --- a/src/Sylius/Behat/Context/Setup/OrderContext.php +++ b/src/Sylius/Behat/Context/Setup/OrderContext.php @@ -17,7 +17,6 @@ use Behat\Behat\Context\Context; use Doctrine\Persistence\ObjectManager; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; use Sylius\Behat\Service\SharedStorageInterface; -use Sylius\Calendar\Provider\DateTimeProviderInterface; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPricingInterface; @@ -45,6 +44,7 @@ use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Shipping\Repository\ShippingMethodRepositoryInterface; use Sylius\Component\Shipping\ShipmentTransitions; +use Symfony\Component\Clock\ClockInterface; use Webmozart\Assert\Assert; final class OrderContext implements Context @@ -65,7 +65,7 @@ final class OrderContext implements Context private ProductVariantResolverInterface $variantResolver, private OrderItemQuantityModifierInterface $itemQuantityModifier, private ObjectManager $objectManager, - private DateTimeProviderInterface $dateTimeProvider, + private ClockInterface $clock, ) { } @@ -920,7 +920,7 @@ final class OrderContext implements Context $customer->setFirstname('John'); $customer->setLastname('Doe' . $i); - $customer->setCreatedAt($this->dateTimeProvider->now()); + $customer->setCreatedAt($this->clock->now()); $customers[] = $customer; @@ -1024,7 +1024,7 @@ final class OrderContext implements Context $this->shipOrder($order); } - $order->setCheckoutCompletedAt($this->dateTimeProvider->now()); + $order->setCheckoutCompletedAt($this->clock->now()); $this->objectManager->persist($order); $this->sharedStorage->set('order', $order); diff --git a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml index b06f40e1f4..7f203f4a0c 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml @@ -123,7 +123,7 @@ - + diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml index 0d795243ba..b34f82f408 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml @@ -24,6 +24,7 @@ default: - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.calendar - sylius.behat.context.setup.channel - sylius.behat.context.setup.locale - sylius.behat.context.setup.product @@ -36,7 +37,6 @@ default: - sylius.behat.context.setup.taxonomy - Sylius\Behat\Context\Setup\CatalogPromotionContext - Sylius\Behat\Context\Setup\PriceHistoryContext - - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.api.admin.managing_products - Sylius\Behat\Context\Api\Admin\BrowsingCatalogPromotionProductVariantsContext diff --git a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml index e83a16d9a4..f24dee2d6c 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml +++ b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml @@ -8,6 +8,7 @@ default: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.hook.session + - sylius.behat.context.setup.calendar - sylius.behat.context.setup.channel - sylius.behat.context.setup.currency - sylius.behat.context.setup.geographical @@ -18,7 +19,6 @@ default: - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.shipping - sylius.behat.context.setup.zone - - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.transform.channel - sylius.behat.context.transform.customer diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml index 71fb8bc930..efc10aea49 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml @@ -24,6 +24,7 @@ default: - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.calendar - sylius.behat.context.setup.channel - sylius.behat.context.setup.locale - sylius.behat.context.setup.product @@ -36,7 +37,6 @@ default: - sylius.behat.context.setup.taxonomy - Sylius\Behat\Context\Setup\CatalogPromotionContext - Sylius\Behat\Context\Setup\PriceHistoryContext - - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.ui.admin.managing_product_attributes - sylius.behat.context.ui.admin.product_showpage