From c29a07614d334b28a16f9075eea54494c5d99461 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Fri, 16 Feb 2024 12:36:23 +0100 Subject: [PATCH 1/9] [Promotion] Add archivedAt to Promotion --- .../Factory/PromotionExampleFactory.php | 6 +++++ .../CoreBundle/Fixture/PromotionFixture.php | 1 + .../Migrations/Version20240216110238.php | 26 +++++++++++++++++++ .../Migrations/Version20240216110239.php | 26 +++++++++++++++++++ .../config/app/fixtures/promotion.yaml | 17 ++++++++++++ .../config/doctrine/model/Promotion.orm.xml | 1 + .../Component/Promotion/Model/Promotion.php | 3 ++- 7 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php create mode 100644 src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php index 90c17fab73..eb7a4e6fb8 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php @@ -93,6 +93,10 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF $promotion->setEndsAt(new \DateTime($options['ends_at'])); } + if (isset($options['archived_at'])) { + $promotion->setArchivedAt(new \DateTime($options['archived_at'])); + } + foreach ($options['channels'] as $channel) { $promotion->addChannel($channel); } @@ -139,6 +143,8 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF ->setAllowedTypes('starts_at', ['null', 'string']) ->setDefault('ends_at', null) ->setAllowedTypes('ends_at', ['null', 'string']) + ->setDefault('archived_at', null) + ->setAllowedTypes('archived_at', ['null', 'string']) ->setDefault('channels', LazyOption::all($this->channelRepository)) ->setAllowedTypes('channels', 'array') ->setNormalizer('channels', LazyOption::findBy($this->channelRepository, 'code')) diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php index 87b79cdb15..ede34631e9 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php @@ -40,6 +40,7 @@ class PromotionFixture extends AbstractResourceFixture ->end() ->scalarNode('starts_at')->cannotBeEmpty()->end() ->scalarNode('ends_at')->cannotBeEmpty()->end() + ->scalarNode('archived_at')->defaultNull()->end() ->arrayNode('rules') ->requiresAtLeastOneElement() ->arrayPrototype() diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php new file mode 100644 index 0000000000..422ec1d946 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE sylius_promotion ADD archived_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_promotion DROP archived_at'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php new file mode 100644 index 0000000000..c5f04eb8ad --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE sylius_promotion ADD archived_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_promotion DROP archived_at'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/app/fixtures/promotion.yaml b/src/Sylius/Bundle/CoreBundle/Resources/config/app/fixtures/promotion.yaml index d8dd316738..bfdbb031ac 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/app/fixtures/promotion.yaml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/app/fixtures/promotion.yaml @@ -40,6 +40,23 @@ sylius_fixtures: configuration: FASHION_WEB: amount: 10.00 + back_to_school: + code: 'back_to_school' + name: 'Back to school' + channels: + - 'FASHION_WEB' + starts_at: 'September 1 last year' + ends_at: 'September 30 last year' + archived_at: 'October 1 last year' + rules: + - type: 'item_total' + configuration: + FASHION_WEB: + amount: 100.00 + actions: + - type: 'order_percentage_discount' + configuration: + percentage: 10 catalog_promotion: options: diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml index b4d1ef3b87..7a54f32412 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml @@ -34,6 +34,7 @@ + diff --git a/src/Sylius/Component/Promotion/Model/Promotion.php b/src/Sylius/Component/Promotion/Model/Promotion.php index 4a91deac07..b6a6669ad7 100644 --- a/src/Sylius/Component/Promotion/Model/Promotion.php +++ b/src/Sylius/Component/Promotion/Model/Promotion.php @@ -15,13 +15,14 @@ namespace Sylius\Component\Promotion\Model; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Sylius\Component\Resource\Model\ArchivableTrait; use Sylius\Component\Resource\Model\TimestampableTrait; use Sylius\Component\Resource\Model\TranslatableTrait; use Sylius\Component\Resource\Model\TranslationInterface; class Promotion implements PromotionInterface { - use TimestampableTrait, TranslatableTrait { + use ArchivableTrait, TimestampableTrait, TranslatableTrait { __construct as private initializeTranslationsCollection; getTranslation as private doGetTranslation; } From ef40b506d9cf7464bd6261b0706a484398c778f1 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Fri, 16 Feb 2024 16:40:44 +0100 Subject: [PATCH 2/9] [Promotion] Add promotion archiving --- .../Resources/config/grids/promotion.yml | 8 ++++++++ .../Resources/config/routing/promotion.yml | 15 +++++++++++++++ .../Promotion/Model/PromotionInterface.php | 3 ++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml index 0e9782964e..839202b661 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml @@ -51,6 +51,12 @@ sylius_grid: label: sylius.ui.coupon options: fields: [coupons.code] + archival: + type: exists + label: sylius.ui.archival + options: + field: archivedAt + default_value: false actions: main: create: @@ -85,6 +91,8 @@ sylius_grid: type: update delete: type: delete + archive: + type: archive bulk: delete: type: delete diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml index 9e9c757342..8489d7bdbd 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml @@ -19,3 +19,18 @@ sylius_admin_promotion: form: "@SyliusAdmin/Promotion/_form.html.twig" toolbar: "@SyliusAdmin/Promotion/_toolbar.html.twig" type: sylius.resource + +sylius_admin_promotion_archive: + path: /promotions/{id}/archive + methods: [PATCH] + defaults: + _controller: sylius.controller.promotion::updateAction + _sylius: + section: admin + permission: true + template: '@SyliusUi/Grid/Action/archive.html.twig' + form: + type: Sylius\Bundle\ResourceBundle\Form\Type\ArchivableType + redirect: + route: sylius_admin_promotion_index + parameters: {} diff --git a/src/Sylius/Component/Promotion/Model/PromotionInterface.php b/src/Sylius/Component/Promotion/Model/PromotionInterface.php index 522a17a4d0..556a1b1fa6 100644 --- a/src/Sylius/Component/Promotion/Model/PromotionInterface.php +++ b/src/Sylius/Component/Promotion/Model/PromotionInterface.php @@ -14,12 +14,13 @@ declare(strict_types=1); namespace Sylius\Component\Promotion\Model; use Doctrine\Common\Collections\Collection; +use Sylius\Component\Resource\Model\ArchivableInterface; use Sylius\Component\Resource\Model\CodeAwareInterface; use Sylius\Component\Resource\Model\ResourceInterface; use Sylius\Component\Resource\Model\TimestampableInterface; use Sylius\Component\Resource\Model\TranslatableInterface; -interface PromotionInterface extends CodeAwareInterface, TimestampableInterface, TranslatableInterface, ResourceInterface +interface PromotionInterface extends ArchivableInterface, CodeAwareInterface, TimestampableInterface, TranslatableInterface, ResourceInterface { public function getName(): ?string; From f8620e75cd6e9ffcc1bbfb343be5cb55f1204bb1 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Fri, 16 Feb 2024 16:41:30 +0100 Subject: [PATCH 3/9] [Promotion][Behat] Add promotion archiving tests --- .../archiving_promotions.feature | 41 +++++++++++++++++ .../Domain/ManagingPromotionsContext.php | 20 +++++++++ .../Behat/Context/Setup/PromotionContext.php | 10 +++++ .../Ui/Admin/ManagingPromotionsContext.php | 45 ++++++++++++++++++- .../Behat/Page/Admin/Promotion/IndexPage.php | 17 +++++++ .../Admin/Promotion/IndexPageInterface.php | 2 + .../config/services/contexts/domain.xml | 1 + 7 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 features/promotion/managing_promotions/archiving_promotions.feature diff --git a/features/promotion/managing_promotions/archiving_promotions.feature b/features/promotion/managing_promotions/archiving_promotions.feature new file mode 100644 index 0000000000..b1a8aa1b84 --- /dev/null +++ b/features/promotion/managing_promotions/archiving_promotions.feature @@ -0,0 +1,41 @@ +@managing_promotions +Feature: Archiving promotions + In order to hide no longer available promotions from the list and customers' use + As a Administrator + I want to archive promotions + + Background: + Given the store operates on a single channel in "United States" + And there is a promotion "Christmas sale" + And there is also a promotion "New Year sale" + And I am logged in as an administrator + + @ui + Scenario: Archiving a promotion + When I browse promotions + And I archive the "Christmas sale" promotion + Then I should see a single promotion in the list + And I should see the promotion "New Year sale" in the list + + @domain + Scenario: Archiving a promotion does not remove it form the database + When I archive the "Christmas sale" promotion + Then the promotion "Christmas sale" should still exist in the registry + + @ui + Scenario: Seeing only archived promotions + Given the promotion "Christmas sale" is archival + When I browse promotions + And I filter archival promotions + Then I should see a single promotion in the list + And I should see the promotion "Christmas sale" in the list + And I should not see the promotion "New Year sale" in the list + + @ui + Scenario: Restoring an archival promotion + Given the promotion "Christmas sale" is archival + When I browse promotions + And I filter archival promotions + And I restore the "Christmas sale" promotion + Then I should be viewing non archival promotions + And I should see 2 promotions on the list diff --git a/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php index 74dd507bb5..9983045f87 100644 --- a/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php @@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Domain; use Behat\Behat\Context\Context; use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; +use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Promotion\Model\PromotionInterface; use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface; @@ -25,6 +26,7 @@ final class ManagingPromotionsContext implements Context public function __construct( private SharedStorageInterface $sharedStorage, private PromotionRepositoryInterface $promotionRepository, + private ObjectManager $promotionManager, ) { } @@ -48,6 +50,16 @@ final class ManagingPromotionsContext implements Context } } + /** + * @When /^I archive the ("[^"]+" promotion)$/ + */ + public function iArchiveThePromotion(PromotionInterface $promotion): void + { + $promotion->setArchivedAt(new \DateTime()); + + $this->promotionManager->flush(); + } + /** * @Then /^(this promotion) should no longer exist in the promotion registry$/ */ @@ -71,4 +83,12 @@ final class ManagingPromotionsContext implements Context { Assert::isInstanceOf($this->sharedStorage->get('last_exception'), ForeignKeyConstraintViolationException::class); } + + /** + * @Then the promotion :promotion should still exist in the registry + */ + public function thePromotionShouldStillExistInTheRegistry(PromotionInterface $promotion): void + { + Assert::notNull($this->promotionRepository->find($promotion)); + } } diff --git a/src/Sylius/Behat/Context/Setup/PromotionContext.php b/src/Sylius/Behat/Context/Setup/PromotionContext.php index 398046b3d6..f7963429de 100644 --- a/src/Sylius/Behat/Context/Setup/PromotionContext.php +++ b/src/Sylius/Behat/Context/Setup/PromotionContext.php @@ -281,6 +281,16 @@ final class PromotionContext implements Context $this->objectManager->flush(); } + /** + * @Given /^the (promotion "[^"]+") is archival$/ + */ + public function thisPromotionIsArchival(PromotionInterface $promotion): void + { + $promotion->setArchivedAt(new \DateTime()); + + $this->objectManager->flush(); + } + /** * @Given /^(this coupon) has already expired$/ */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php index ca2b958299..3b9cd6403c 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php @@ -96,8 +96,6 @@ final class ManagingPromotionsContext implements Context */ public function thePromotionShouldAppearInTheRegistry(string $promotionName): void { - $this->indexPage->open(); - Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $promotionName])); } @@ -278,6 +276,33 @@ final class ManagingPromotionsContext implements Context $this->indexPage->bulkDelete(); } + /** + * @When I archive the :promotionName promotion + */ + public function iArchiveThePromotion(string $promotionName): void + { + $actions = $this->indexPage->getActionsForResource(['name' => $promotionName]); + $actions->pressButton('Archive'); + } + + /** + * @When I restore the :promotionName promotion + */ + public function iRestoreThePromotion(string $promotionName): void + { + $actions = $this->indexPage->getActionsForResource(['name' => $promotionName]); + $actions->pressButton('Restore'); + } + + /** + * @When I filter archival promotions + */ + public function iFilterArchivalPromotions(): void + { + $this->indexPage->chooseArchival('Yes'); + $this->indexPage->filter(); + } + /** * @Then I should see a single promotion in the list * @Then there should be :amount promotions @@ -829,6 +854,22 @@ final class ManagingPromotionsContext implements Context ); } + /** + * @Then I should not see the promotion :promotionName in the list + */ + public function iShouldNotSeeThePromotionInTheList(string $promotionName): void + { + Assert::false($this->indexPage->isSingleResourceOnPage(['name' => $promotionName])); + } + + /** + * @Then I should be viewing non archival promotions + */ + public function iShouldBeViewingNonArchivalPromotions(): void + { + Assert::false($this->indexPage->isArchivalFilterEnabled()); + } + private function assertFieldValidationMessage(string $element, string $expectedMessage) { /** @var CreatePageInterface|UpdatePageInterface $currentPage */ diff --git a/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php b/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php index 132562ac7d..253f27f35a 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php @@ -50,6 +50,23 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface $this->getDocument()->fillField(sprintf('criteria_%s_value', $field), $value); } + public function chooseArchival(string $isArchival): void + { + $this->getElement('filter_archival')->selectOption($isArchival); + } + + public function isArchivalFilterEnabled(): bool + { + return '1' === $this->getElement('filter_archival')->getValue(); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'filter_archival' => '#criteria_archival', + ]); + } + private function getPromotionFieldsWithHeader(PromotionInterface $promotion, string $header): NodeElement { $tableAccessor = $this->getTableAccessor(); diff --git a/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php b/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php index c666e3bcd4..f9e3d77f49 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php @@ -23,4 +23,6 @@ interface IndexPageInterface extends BaseIndexPageInterface public function isAbleToManageCouponsFor(PromotionInterface $promotion): bool; public function isCouponBasedFor(PromotionInterface $promotion): bool; + + public function chooseArchival(string $isArchival): void; } diff --git a/src/Sylius/Behat/Resources/config/services/contexts/domain.xml b/src/Sylius/Behat/Resources/config/services/contexts/domain.xml index dfca36a3b0..f963892fab 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/domain.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/domain.xml @@ -49,6 +49,7 @@ + From d50ec85545faa3e58d12b7d755afafb737f10d9e Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Fri, 16 Feb 2024 18:44:34 +0100 Subject: [PATCH 4/9] [Promotion] Add PromotionArchivalEligibilityChecker class --- .../cart_promotion_integration.feature | 26 +++++++++++ .../archiving_promotions.feature | 4 +- .../Behat/Context/Setup/PromotionContext.php | 4 +- .../config/suites/ui/cart/shopping_cart.yml | 1 + .../config/services/eligibility_checkers.xml | 5 +++ .../PromotionArchivalEligibilityChecker.php | 25 +++++++++++ ...romotionArchivalEligibilityCheckerSpec.php | 45 +++++++++++++++++++ 7 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 features/cart/shopping_cart/cart_promotion_integration.feature create mode 100644 src/Sylius/Component/Promotion/Checker/Eligibility/PromotionArchivalEligibilityChecker.php create mode 100644 src/Sylius/Component/Promotion/spec/Checker/Eligibility/PromotionArchivalEligibilityCheckerSpec.php diff --git a/features/cart/shopping_cart/cart_promotion_integration.feature b/features/cart/shopping_cart/cart_promotion_integration.feature new file mode 100644 index 0000000000..5ac7f0deb3 --- /dev/null +++ b/features/cart/shopping_cart/cart_promotion_integration.feature @@ -0,0 +1,26 @@ +@shopping_cart +Feature: Cart promotions integrity + In order to ensure only valid promotions are applied to the cart + As a Visitor + I want to see the promotions that are applied to the cart + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "PHP T-Shirt" priced at "$100.00" + And there is a promotion "Christmas sale" + And this promotion gives "$10.00" discount to every order + + @ui + Scenario: Promotion is applied to the cart when it is valid + When I add product "PHP T-Shirt" to the cart + Then I should be on my cart summary page + And I should be notified that the product has been successfully added + And my cart total should be "$90.00" + + @ui + Scenario: Archived promotion is not applied to the cart + Given the promotion "Christmas sale" is archived + When I add product "PHP T-Shirt" to the cart + Then I should be on my cart summary page + And I should be notified that the product has been successfully added + And my cart total should be "$100.00" diff --git a/features/promotion/managing_promotions/archiving_promotions.feature b/features/promotion/managing_promotions/archiving_promotions.feature index b1a8aa1b84..b035cd0396 100644 --- a/features/promotion/managing_promotions/archiving_promotions.feature +++ b/features/promotion/managing_promotions/archiving_promotions.feature @@ -24,7 +24,7 @@ Feature: Archiving promotions @ui Scenario: Seeing only archived promotions - Given the promotion "Christmas sale" is archival + Given the promotion "Christmas sale" is archived When I browse promotions And I filter archival promotions Then I should see a single promotion in the list @@ -33,7 +33,7 @@ Feature: Archiving promotions @ui Scenario: Restoring an archival promotion - Given the promotion "Christmas sale" is archival + Given the promotion "Christmas sale" is archived When I browse promotions And I filter archival promotions And I restore the "Christmas sale" promotion diff --git a/src/Sylius/Behat/Context/Setup/PromotionContext.php b/src/Sylius/Behat/Context/Setup/PromotionContext.php index f7963429de..555b75b984 100644 --- a/src/Sylius/Behat/Context/Setup/PromotionContext.php +++ b/src/Sylius/Behat/Context/Setup/PromotionContext.php @@ -282,9 +282,9 @@ final class PromotionContext implements Context } /** - * @Given /^the (promotion "[^"]+") is archival$/ + * @Given /^the (promotion "[^"]+") is archived$/ */ - public function thisPromotionIsArchival(PromotionInterface $promotion): void + public function thisPromotionIsArchived(PromotionInterface $promotion): void { $promotion->setArchivedAt(new \DateTime()); diff --git a/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml b/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml index 8e018b6ae2..5ff03e8538 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml @@ -15,6 +15,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.zone diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml index 62b13a7ca8..caf2fed7e3 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml @@ -55,5 +55,10 @@ class="Sylius\Component\Promotion\Checker\Eligibility\CompositePromotionEligibilityChecker"> + + + diff --git a/src/Sylius/Component/Promotion/Checker/Eligibility/PromotionArchivalEligibilityChecker.php b/src/Sylius/Component/Promotion/Checker/Eligibility/PromotionArchivalEligibilityChecker.php new file mode 100644 index 0000000000..bb8aeb645a --- /dev/null +++ b/src/Sylius/Component/Promotion/Checker/Eligibility/PromotionArchivalEligibilityChecker.php @@ -0,0 +1,25 @@ +getArchivedAt(); + } +} diff --git a/src/Sylius/Component/Promotion/spec/Checker/Eligibility/PromotionArchivalEligibilityCheckerSpec.php b/src/Sylius/Component/Promotion/spec/Checker/Eligibility/PromotionArchivalEligibilityCheckerSpec.php new file mode 100644 index 0000000000..7ffc63bfca --- /dev/null +++ b/src/Sylius/Component/Promotion/spec/Checker/Eligibility/PromotionArchivalEligibilityCheckerSpec.php @@ -0,0 +1,45 @@ +shouldImplement(PromotionEligibilityCheckerInterface::class); + } + + function it_is_eligible_when_archived_at_is_null( + PromotionSubjectInterface $promotionSubject, + PromotionInterface $promotion, + ): void { + $promotion->getArchivedAt()->willReturn(null); + + $this->isEligible($promotionSubject, $promotion)->shouldReturn(true); + } + + function it_is_not_eligible_when_archived_at_is_not_null( + PromotionSubjectInterface $promotionSubject, + PromotionInterface $promotion, + ): void { + $promotion->getArchivedAt()->willReturn(new \DateTime()); + + $this->isEligible($promotionSubject, $promotion)->shouldReturn(false); + } +} From 09b1a09f9d3c31fdc656e95b0699799e0bb5b28e Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Sat, 17 Feb 2024 20:31:29 +0100 Subject: [PATCH 5/9] [Api][Promotion] Add archivedAt to promotion --- .../cart_promotion_integration.feature | 4 +- .../archiving_promotions.feature | 6 +- .../Api/Admin/ManagingPromotionsContext.php | 46 ++++++++++++++ .../Domain/ManagingPromotionsContext.php | 2 +- .../Behat/Context/Setup/PromotionContext.php | 2 +- .../config/suites/api/cart/shopping_cart.yml | 1 + .../ArchivingPromotionApplicator.php | 39 ++++++++++++ .../ArchivingPromotionApplicatorInterface.php | 24 ++++++++ .../HideArchivedPromotionExtension.php | 46 ++++++++++++++ .../config/api_resources/Promotion.xml | 40 ++++++++++++ .../config/serialization/Promotion.xml | 5 ++ .../Resources/config/services/applicators.xml | 5 ++ .../Resources/config/services/extensions.xml | 5 ++ .../Resources/config/services/filters.xml | 7 +++ .../ArchivingPromotionApplicatorSpec.php | 46 ++++++++++++++ .../HideArchivedPromotionExtensionSpec.php | 61 +++++++++++++++++++ tests/Api/Admin/PromotionsTest.php | 50 +++++++++++++++ .../DataFixtures/ORM/promotion/promotion.yaml | 16 +++++ .../admin/promotion/archive_promotion.json | 32 ++++++++++ .../promotion/get_promotion_response.json | 1 + .../promotion/get_promotions_response.json | 10 ++- .../promotion/post_promotion_response.json | 1 + .../promotion/put_promotion_response.json | 1 + ...y_when_priority_is_minus_one_response.json | 3 +- .../admin/promotion/restore_promotion.json | 31 ++++++++++ 25 files changed, 475 insertions(+), 9 deletions(-) create mode 100644 src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php create mode 100644 src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php create mode 100644 src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php create mode 100644 src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php create mode 100644 src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php create mode 100644 tests/Api/Responses/admin/promotion/archive_promotion.json create mode 100644 tests/Api/Responses/admin/promotion/restore_promotion.json diff --git a/features/cart/shopping_cart/cart_promotion_integration.feature b/features/cart/shopping_cart/cart_promotion_integration.feature index 5ac7f0deb3..06837eed20 100644 --- a/features/cart/shopping_cart/cart_promotion_integration.feature +++ b/features/cart/shopping_cart/cart_promotion_integration.feature @@ -10,14 +10,14 @@ Feature: Cart promotions integrity And there is a promotion "Christmas sale" And this promotion gives "$10.00" discount to every order - @ui + @api @ui Scenario: Promotion is applied to the cart when it is valid When I add product "PHP T-Shirt" to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And my cart total should be "$90.00" - @ui + @api @ui Scenario: Archived promotion is not applied to the cart Given the promotion "Christmas sale" is archived When I add product "PHP T-Shirt" to the cart diff --git a/features/promotion/managing_promotions/archiving_promotions.feature b/features/promotion/managing_promotions/archiving_promotions.feature index b035cd0396..86ff7f6a46 100644 --- a/features/promotion/managing_promotions/archiving_promotions.feature +++ b/features/promotion/managing_promotions/archiving_promotions.feature @@ -10,7 +10,7 @@ Feature: Archiving promotions And there is also a promotion "New Year sale" And I am logged in as an administrator - @ui + @api @ui Scenario: Archiving a promotion When I browse promotions And I archive the "Christmas sale" promotion @@ -22,7 +22,7 @@ Feature: Archiving promotions When I archive the "Christmas sale" promotion Then the promotion "Christmas sale" should still exist in the registry - @ui + @api @ui Scenario: Seeing only archived promotions Given the promotion "Christmas sale" is archived When I browse promotions @@ -31,7 +31,7 @@ Feature: Archiving promotions And I should see the promotion "Christmas sale" in the list And I should not see the promotion "New Year sale" in the list - @ui + @api @ui Scenario: Restoring an archival promotion Given the promotion "Christmas sale" is archived When I browse promotions diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php index 55c3da2c08..722219236b 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php @@ -34,6 +34,7 @@ use Sylius\Component\Core\Promotion\Checker\Rule\HasTaxonRuleChecker; use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker; use Sylius\Component\Customer\Model\CustomerGroupInterface; use Sylius\Component\Promotion\Checker\Rule\ItemTotalRuleChecker; +use Symfony\Component\HttpFoundation\Request; use Webmozart\Assert\Assert; final class ManagingPromotionsContext implements Context @@ -73,6 +74,23 @@ final class ManagingPromotionsContext implements Context $this->client->buildUpdateRequest(Resources::PROMOTIONS, $promotion->getCode()); } + /** + * @When I archive the :promotion promotion + */ + public function iArchiveThePromotion(PromotionInterface $promotion): void + { + $this->client->customItemAction(Resources::PROMOTIONS, $promotion->getCode(), Request::METHOD_PATCH, 'archive'); + $this->client->index(Resources::PROMOTIONS); + } + + /** + * @When I restore the :promotion promotion + */ + public function iRestoreThePromotion(PromotionInterface $promotion): void + { + $this->client->customItemAction(Resources::PROMOTIONS, $promotion->getCode(), Request::METHOD_PATCH, 'restore'); + } + /** * @When I specify its :field as :value * @When I do not specify its :field @@ -410,6 +428,15 @@ final class ManagingPromotionsContext implements Context $this->client->filter(); } + /** + * @When I filter archival promotions + */ + public function iFilterArchivalPromotions(): void + { + $this->client->addFilter('exists[archivedAt]', true); + $this->client->filter(); + } + /** * @When I add it * @When I try to add it @@ -456,6 +483,17 @@ final class ManagingPromotionsContext implements Context ); } + /** + * @Then I should not see the promotion :promotionName in the list + */ + public function iShouldNotSeeThePromotionInTheList(string $promotionName): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->getLastResponse(), 'name', $promotionName), + sprintf('Promotion with name %s does not exist', $promotionName), + ); + } + /** * @Then /^(this promotion) should be coupon based$/ */ @@ -847,6 +885,14 @@ final class ManagingPromotionsContext implements Context ); } + /** + * @Then I should be viewing non archival promotions + */ + public function iShouldBeViewingNonArchivalPromotions(): void + { + $this->client->index(Resources::PROMOTIONS); + } + private function addToRequestAction(string $type, array $configuration): void { $data['actions'][] = [ diff --git a/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php index 9983045f87..2c3442e601 100644 --- a/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php @@ -51,7 +51,7 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I archive the ("[^"]+" promotion)$/ + * @When I archive the :promotion promotion */ public function iArchiveThePromotion(PromotionInterface $promotion): void { diff --git a/src/Sylius/Behat/Context/Setup/PromotionContext.php b/src/Sylius/Behat/Context/Setup/PromotionContext.php index 555b75b984..ac28a9db6d 100644 --- a/src/Sylius/Behat/Context/Setup/PromotionContext.php +++ b/src/Sylius/Behat/Context/Setup/PromotionContext.php @@ -282,7 +282,7 @@ final class PromotionContext implements Context } /** - * @Given /^the (promotion "[^"]+") is archived$/ + * @Given the promotion :promotion is archived */ public function thisPromotionIsArchived(PromotionInterface $promotion): void { diff --git a/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml b/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml index 42175300e8..94c5f33328 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml @@ -23,6 +23,7 @@ default: - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php new file mode 100644 index 0000000000..d2ad96ca47 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php @@ -0,0 +1,39 @@ +setArchivedAt($this->calendar->now()); + + return $data; + } + + public function restore(PromotionInterface $data): PromotionInterface + { + $data->setArchivedAt(null); + + return $data; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php new file mode 100644 index 0000000000..556c59ed63 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php @@ -0,0 +1,24 @@ +promotionClass !== $resourceClass) { + return; + } + + if (isset($context['filters']['exists']['archivedAt'])) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder->andWhere($queryBuilder->expr()->isNull(sprintf('%s.archivedAt', $rootAlias))); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml index 921e1775a5..4aaa5dfd65 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml @@ -41,6 +41,8 @@ admin:promotion:show sylius:admin:promotion:show + + false @@ -81,10 +83,13 @@ Example configuration for `order_fixed_discount` action type: admin:promotion:index sylius:admin:promotion:index + + false sylius.api.promotion_coupon_search_filter sylius.api.promotion_order_filter + sylius.api.exists_filter.archived_at @@ -97,6 +102,8 @@ Example configuration for `order_fixed_discount` action type: admin:promotion:show sylius:admin:promotion:show + + false @@ -113,6 +120,8 @@ Example configuration for `order_fixed_discount` action type: admin:promotion:show sylius:admin:promotion:show + + false @@ -146,6 +155,37 @@ Example configuration for `order_fixed_discount` action type: + + PATCH + /promotions/{code}/archive + false + Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface::archive + + Archives Promotion + + + + admin:promotion:read + sylius:admin:promotion:read + + + + + PATCH + /promotions/{code}/restore + false + Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface::restore + + Restores Archived Promotion + + + + admin:promotion:read + sylius:admin:promotion:read + + + + DELETE diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml index ab00720385..cb42f7fbc3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml @@ -198,5 +198,10 @@ admin:promotion:update sylius:admin:promotion:update + + + admin:promotion:read + sylius:admin:promotion:read + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml index 9ee3efa0de..4d5c16938b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml @@ -33,5 +33,10 @@ + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml index e46f384afd..77ab980ce5 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml @@ -140,5 +140,10 @@ + + + %sylius.model.promotion.class% + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml index d8204511e4..e59d20578b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml @@ -410,5 +410,12 @@ + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php new file mode 100644 index 0000000000..58bb26a1f4 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($calendar); + } + + function it_archives_promotion( + DateTimeProviderInterface $calendar, + PromotionInterface $promotion, + ): void { + $now = new \DateTime(); + $calendar->now()->willReturn($now); + + $promotion->setArchivedAt($now)->shouldBeCalledOnce(); + + $this->archive($promotion)->shouldReturn($promotion); + } + + function it_restores_promotion( + PromotionInterface $promotion, + ): void { + $promotion->setArchivedAt(null)->shouldBeCalledOnce(); + + $this->restore($promotion)->shouldReturn($promotion); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php new file mode 100644 index 0000000000..aa2f8f25a6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php @@ -0,0 +1,61 @@ +beConstructedWith('promotionClass'); + } + + function it_does_nothing_if_current_resource_is_not_a_promotion( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + $queryBuilder->andWhere()->shouldNotBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, 'taxonClass', 'get', []); + } + + function it_does_nothing_if_archived_at_filter_is_already_applied( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + $queryBuilder->andWhere()->shouldNotBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, 'promotionClass', 'get', ['filters' => ['exists' => ['archivedAt' => 'true']]]); + } + + function it_filters_archived_promotions( + QueryBuilder $queryBuilder, + Expr $expr, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->willReturn(['o']); + + $expr->isNull('o.archivedAt')->willReturn('o.archivedAt IS NULL'); + $queryBuilder->expr()->willReturn($expr); + $queryBuilder->andWhere('o.archivedAt IS NULL')->shouldBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, 'promotionClass', 'get', []); + } +} diff --git a/tests/Api/Admin/PromotionsTest.php b/tests/Api/Admin/PromotionsTest.php index bc5f5028c4..e9cd400b94 100644 --- a/tests/Api/Admin/PromotionsTest.php +++ b/tests/Api/Admin/PromotionsTest.php @@ -576,6 +576,56 @@ final class PromotionsTest extends JsonApiTestCase ); } + /** @test */ + public function it_archives_a_promotion(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'promotion/promotion.yaml' + ]); + + /** @var PromotionInterface $promotion */ + $promotion = $fixtures['promotion_50_off']; + + $this->client->request( + method: 'PATCH', + uri: sprintf('/api/v2/admin/promotions/%s/archive', $promotion->getCode()), + server: $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion/archive_promotion', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_restore_a_promotion(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'promotion/promotion.yaml' + ]); + + /** @var PromotionInterface $promotion */ + $promotion = $fixtures['promotion_back_to_school']; + + $this->client->request( + method: 'PATCH', + uri: sprintf('/api/v2/admin/promotions/%s/restore', $promotion->getCode()), + server: $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion/restore_promotion', + Response::HTTP_OK, + ); + } + /** @test */ public function it_deletes_a_promotion(): void { diff --git a/tests/Api/DataFixtures/ORM/promotion/promotion.yaml b/tests/Api/DataFixtures/ORM/promotion/promotion.yaml index bb4496f4ba..b4b76e7c46 100644 --- a/tests/Api/DataFixtures/ORM/promotion/promotion.yaml +++ b/tests/Api/DataFixtures/ORM/promotion/promotion.yaml @@ -23,6 +23,18 @@ Sylius\Component\Core\Model\Promotion: couponBased: true translations: - '@promotion_1_off_en' + promotion_back_to_school: + code: 'back_to_school' + name: 'Back to school' + description: 'Get 10% off on all school supplies' + channels: ['@channel_web', '@channel_mobile'] + priority: 3 + exclusive: false + appliesToDiscounted: true + couponBased: false + archivedAt: + translations: + - '@promotion_back_to_school_en' Sylius\Component\Promotion\Model\PromotionTranslation: promotion_50_off_en: @@ -33,6 +45,10 @@ Sylius\Component\Promotion\Model\PromotionTranslation: locale: 'en_US' label: '1$ off every item!' translatable: '@promotion_1_off' + promotion_back_to_school_en: + locale: 'en_US' + label: 'Back to school sale!' + translatable: '@promotion_back_to_school' Sylius\Component\Core\Model\PromotionCoupon: promotion_1_off_coupon_1: diff --git a/tests/Api/Responses/admin/promotion/archive_promotion.json b/tests/Api/Responses/admin/promotion/archive_promotion.json new file mode 100644 index 0000000000..6f0fe9a43c --- /dev/null +++ b/tests/Api/Responses/admin/promotion/archive_promotion.json @@ -0,0 +1,32 @@ +{ + "@context": "\/api\/v2\/contexts\/Promotion", + "@id": "\/api\/v2\/admin\/promotions\/50_off", + "@type": "Promotion", + "channels": [ + "\/api\/v2\/admin\/channels\/MOBILE" + ], + "id": @integer@, + "code": "50_off", + "name": "50% Off on your first order", + "description": "Get 50% off of your first purchase", + "priority": 1, + "exclusive": true, + "usageLimit": 1, + "used": 0, + "couponBased": false, + "coupons": [], + "rules": [], + "actions": [], + "appliesToDiscounted": false, + "archivedAt": @date@, + "createdAt": @date@, + "updatedAt": @date@, + "translations": { + "en_US": { + "@id": "\/api\/v2\/admin\/promotion-translations\/@integer@", + "@type": "PromotionTranslation", + "id": @integer@, + "label": "-50% on first order!" + } + } +} diff --git a/tests/Api/Responses/admin/promotion/get_promotion_response.json b/tests/Api/Responses/admin/promotion/get_promotion_response.json index 457730d357..c77ad07dd6 100644 --- a/tests/Api/Responses/admin/promotion/get_promotion_response.json +++ b/tests/Api/Responses/admin/promotion/get_promotion_response.json @@ -20,6 +20,7 @@ "coupons": [], "rules": [], "actions": [], + "archivedAt": null, "createdAt": @date@, "updatedAt": @date@, "translations": { diff --git a/tests/Api/Responses/admin/promotion/get_promotions_response.json b/tests/Api/Responses/admin/promotion/get_promotions_response.json index 1b26793039..bb15aa7d3f 100644 --- a/tests/Api/Responses/admin/promotion/get_promotions_response.json +++ b/tests/Api/Responses/admin/promotion/get_promotions_response.json @@ -28,6 +28,7 @@ ], "rules": [], "actions": [], + "archivedAt": null, "createdAt": @date@, "updatedAt": @date@, "translations": { @@ -60,6 +61,7 @@ "coupons": [], "rules": [], "actions": [], + "archivedAt": null, "createdAt": @date@, "updatedAt": @date@, "translations": { @@ -75,7 +77,7 @@ "hydra:totalItems": 2, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "\/api\/v2\/admin\/promotions{?coupons.code,coupons.code[],order[priority]}", + "hydra:template": "\/api\/v2\/admin\/promotions{?coupons.code,coupons.code[],order[priority],exists[archivedAt]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -95,6 +97,12 @@ "variable": "order[priority]", "property": "priority", "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "exists[archivedAt]", + "property": "archivedAt", + "required": false } ] } diff --git a/tests/Api/Responses/admin/promotion/post_promotion_response.json b/tests/Api/Responses/admin/promotion/post_promotion_response.json index f83573b49a..86e5531e9f 100644 --- a/tests/Api/Responses/admin/promotion/post_promotion_response.json +++ b/tests/Api/Responses/admin/promotion/post_promotion_response.json @@ -13,6 +13,7 @@ "exclusive": true, "usageLimit": 3, "used": 0, + "archivedAt": null, "startsAt": "2023-10-04 12:30:00", "endsAt": "2023-11-04 12:30:00", "couponBased": true, diff --git a/tests/Api/Responses/admin/promotion/put_promotion_response.json b/tests/Api/Responses/admin/promotion/put_promotion_response.json index d30e843de7..1f1b68932c 100644 --- a/tests/Api/Responses/admin/promotion/put_promotion_response.json +++ b/tests/Api/Responses/admin/promotion/put_promotion_response.json @@ -13,6 +13,7 @@ "exclusive": true, "usageLimit": 11, "used": 0, + "archivedAt": null, "startsAt": null, "endsAt": null, "couponBased": false, diff --git a/tests/Api/Responses/admin/promotion/put_promotion_to_last_priority_when_priority_is_minus_one_response.json b/tests/Api/Responses/admin/promotion/put_promotion_to_last_priority_when_priority_is_minus_one_response.json index 62d9201094..15cb483c1b 100644 --- a/tests/Api/Responses/admin/promotion/put_promotion_to_last_priority_when_priority_is_minus_one_response.json +++ b/tests/Api/Responses/admin/promotion/put_promotion_to_last_priority_when_priority_is_minus_one_response.json @@ -9,7 +9,7 @@ "code": "50_off", "name": "50% Off on your first order", "description": "Get 50% off of your first purchase", - "priority": 2, + "priority": 3, "exclusive": true, "usageLimit": 1, "used": 0, @@ -20,6 +20,7 @@ "rules": [], "actions": [], "appliesToDiscounted": false, + "archivedAt": null, "createdAt": @date@, "updatedAt": @date@, "translations": { diff --git a/tests/Api/Responses/admin/promotion/restore_promotion.json b/tests/Api/Responses/admin/promotion/restore_promotion.json new file mode 100644 index 0000000000..5b79b834d4 --- /dev/null +++ b/tests/Api/Responses/admin/promotion/restore_promotion.json @@ -0,0 +1,31 @@ +{ + "@context": "\/api\/v2\/contexts\/Promotion", + "@id": "\/api\/v2\/admin\/promotions\/back_to_school", + "@type": "Promotion", + "channels": [ + "\/api\/v2\/admin\/channels\/WEB", + "\/api\/v2\/admin\/channels\/MOBILE" + ], + "id": @integer@, + "code": "back_to_school", + "name": "Back to school", + "description": "Get 10% off on all school supplies", + "priority": 3, + "exclusive": false, + "used": 0, + "couponBased": false, + "coupons": [], + "rules": [], + "actions": [], + "appliesToDiscounted": true, + "createdAt": @date@, + "updatedAt": @date@, + "translations": { + "en_US": { + "@id": "\/api\/v2\/admin\/promotion-translations\/@integer@", + "@type": "PromotionTranslation", + "id": @integer@, + "label": "Back to school sale!" + } + } +} From e493c684f18a0822abb54d33d68371ef5439eb60 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Sun, 18 Feb 2024 12:31:02 +0100 Subject: [PATCH 6/9] Fix phpstan --- .../QueryCollectionExtension/HideArchivedPromotionExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php index a9dad95eb8..f3bbb9ce23 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php @@ -24,6 +24,7 @@ final class HideArchivedPromotionExtension implements ContextAwareQueryCollectio { } + /** @param array $context */ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, From 1214ba8da699a55560785329ca5c7c502101e27e Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Sun, 18 Feb 2024 13:04:41 +0100 Subject: [PATCH 7/9] [Behat][Ui] Add a method to check if a promotion exists without opening the index page --- .../Context/Ui/Admin/ManagingPromotionsContext.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php index 3b9cd6403c..98c0866296 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php @@ -88,7 +88,6 @@ final class ManagingPromotionsContext implements Context } /** - * @Then I should see the promotion :promotionName in the list * @Then the :promotionName promotion should appear in the registry * @Then the :promotionName promotion should exist in the registry * @Then this promotion should still be named :promotionName @@ -96,6 +95,8 @@ final class ManagingPromotionsContext implements Context */ public function thePromotionShouldAppearInTheRegistry(string $promotionName): void { + $this->indexPage->open(); + Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $promotionName])); } @@ -854,6 +855,14 @@ final class ManagingPromotionsContext implements Context ); } + /** + * @Then I should see the promotion :promotionName in the list + */ + public function iShouldSeeThePromotionInTheList(string $promotionName): void + { + Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $promotionName])); + } + /** * @Then I should not see the promotion :promotionName in the list */ From f8f8e60cb65316faca4fc9e6c5c16b93dbd0409b Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Mon, 19 Feb 2024 08:05:08 +0100 Subject: [PATCH 8/9] [Behat][Ui] Add missing method --- src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php b/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php index f9e3d77f49..cae26fffd9 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php @@ -25,4 +25,6 @@ interface IndexPageInterface extends BaseIndexPageInterface public function isCouponBasedFor(PromotionInterface $promotion): bool; public function chooseArchival(string $isArchival): void; + + public function isArchivalFilterEnabled(): bool; } From e2afc651280b5e751072a5f63d3ac18b369ad9db Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Thu, 22 Feb 2024 12:45:36 +0100 Subject: [PATCH 9/9] [Behat] Move archived promotion test to receiving_discount folder --- .../order_promotion_integrity_validation.feature | 11 +++++++++++ .../archiving_promotions.feature | 4 ++-- .../not_applying_archived_promotions.feature} | 15 ++++----------- .../config/suites/api/cart/shopping_cart.yml | 1 - .../config/suites/api/checkout/checkout.yml | 1 + .../suites/api/promotion/receiving_discount.yml | 1 + .../config/suites/ui/cart/shopping_cart.yml | 1 - .../config/suites/ui/checkout/checkout.yml | 1 + .../suites/ui/promotion/receiving_discount.yml | 1 + tests/Api/Admin/PromotionsTest.php | 2 +- 10 files changed, 22 insertions(+), 16 deletions(-) rename features/{cart/shopping_cart/cart_promotion_integration.feature => promotion/receiving_discount/not_applying_archived_promotions.feature} (57%) diff --git a/features/checkout/order_promotion/order_promotion_integrity_validation.feature b/features/checkout/order_promotion/order_promotion_integrity_validation.feature index f73b72d25c..7a73ec035a 100644 --- a/features/checkout/order_promotion/order_promotion_integrity_validation.feature +++ b/features/checkout/order_promotion/order_promotion_integrity_validation.feature @@ -73,3 +73,14 @@ Feature: Order promotions integrity And I proceeded with "Free" shipping method and "Offline" payment method When I confirm my order Then I should see the thank you page + + @ui @api + Scenario: Preventing customer from completing checkout with already archived promotion + Given this promotion gives "$10.00" discount to every order + And I added product "PHP T-Shirt" to the cart + And I have specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And I proceeded with "Free" shipping method and "Offline" payment method + And the promotion "Christmas sale" is archived + When I try to confirm my order + Then I should be informed that this promotion is no longer applied + And I should not see the thank you page diff --git a/features/promotion/managing_promotions/archiving_promotions.feature b/features/promotion/managing_promotions/archiving_promotions.feature index 86ff7f6a46..2ffb43e42d 100644 --- a/features/promotion/managing_promotions/archiving_promotions.feature +++ b/features/promotion/managing_promotions/archiving_promotions.feature @@ -1,7 +1,7 @@ @managing_promotions Feature: Archiving promotions In order to hide no longer available promotions from the list and customers' use - As a Administrator + As an Administrator I want to archive promotions Background: @@ -18,7 +18,7 @@ Feature: Archiving promotions And I should see the promotion "New Year sale" in the list @domain - Scenario: Archiving a promotion does not remove it form the database + Scenario: Archiving a promotion does not remove it from the database When I archive the "Christmas sale" promotion Then the promotion "Christmas sale" should still exist in the registry diff --git a/features/cart/shopping_cart/cart_promotion_integration.feature b/features/promotion/receiving_discount/not_applying_archived_promotions.feature similarity index 57% rename from features/cart/shopping_cart/cart_promotion_integration.feature rename to features/promotion/receiving_discount/not_applying_archived_promotions.feature index 06837eed20..139319b269 100644 --- a/features/cart/shopping_cart/cart_promotion_integration.feature +++ b/features/promotion/receiving_discount/not_applying_archived_promotions.feature @@ -1,8 +1,8 @@ -@shopping_cart -Feature: Cart promotions integrity - In order to ensure only valid promotions are applied to the cart +@receiving_discount +Feature: Not applying archived promotions to the cart + In order to not apply promotions that are archived As a Visitor - I want to see the promotions that are applied to the cart + I want to not receive discounts from archived promotions Background: Given the store operates on a single channel in "United States" @@ -10,13 +10,6 @@ Feature: Cart promotions integrity And there is a promotion "Christmas sale" And this promotion gives "$10.00" discount to every order - @api @ui - Scenario: Promotion is applied to the cart when it is valid - When I add product "PHP T-Shirt" to the cart - Then I should be on my cart summary page - And I should be notified that the product has been successfully added - And my cart total should be "$90.00" - @api @ui Scenario: Archived promotion is not applied to the cart Given the promotion "Christmas sale" is archived diff --git a/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml b/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml index 94c5f33328..42175300e8 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/cart/shopping_cart.yml @@ -23,7 +23,6 @@ default: - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product - sylius.behat.context.transform.product_variant - - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category diff --git a/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml b/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml index 7d0ed5b26e..8114210110 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml @@ -18,6 +18,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.province - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml index 245216938a..f495e12771 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml @@ -15,6 +15,7 @@ default: - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.taxon - sylius.behat.context.transform.shipping_method diff --git a/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml b/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml index 5ff03e8538..8e018b6ae2 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/cart/shopping_cart.yml @@ -15,7 +15,6 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant - - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.zone diff --git a/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml b/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml index 5a6d578dbf..808732cd01 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml @@ -21,6 +21,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.province - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category diff --git a/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml b/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml index 4df4b04cb6..0ddfca73ee 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml @@ -13,6 +13,7 @@ default: - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.taxon diff --git a/tests/Api/Admin/PromotionsTest.php b/tests/Api/Admin/PromotionsTest.php index e9cd400b94..a2438a16da 100644 --- a/tests/Api/Admin/PromotionsTest.php +++ b/tests/Api/Admin/PromotionsTest.php @@ -602,7 +602,7 @@ final class PromotionsTest extends JsonApiTestCase } /** @test */ - public function it_restore_a_promotion(): void + public function it_restores_a_promotion(): void { $fixtures = $this->loadFixturesFromFiles([ 'authentication/api_administrator.yaml',