diff --git a/features/promotion/managing_coupons/adding_coupon.feature b/features/promotion/managing_coupons/adding_coupon.feature index 8acc91cfb0d..5f7c5ece9b3 100644 --- a/features/promotion/managing_coupons/adding_coupon.feature +++ b/features/promotion/managing_coupons/adding_coupon.feature @@ -10,7 +10,7 @@ Feature: Adding a new coupon And it is coupon based promotion And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new coupon When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" @@ -19,4 +19,4 @@ Feature: Adding a new coupon And I make it valid until "21.04.2017" And I add it Then I should be notified that it has been successfully created - And there should be coupon with code "SANTA2016" + And there should be a "Christmas sale" promotion with a coupon code "SANTA2016" diff --git a/features/promotion/managing_coupons/browsing_coupon.feature b/features/promotion/managing_coupons/browsing_coupons.feature similarity index 77% rename from features/promotion/managing_coupons/browsing_coupon.feature rename to features/promotion/managing_coupons/browsing_coupons.feature index c94595a8f72..e514a73c6be 100644 --- a/features/promotion/managing_coupons/browsing_coupon.feature +++ b/features/promotion/managing_coupons/browsing_coupons.feature @@ -9,8 +9,8 @@ Feature: Browsing promotion coupons And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui - Scenario: Browsing coupons in store + @ui @api + Scenario: Browsing coupons of a promotion When I want to view all coupons of this promotion And there should be 1 coupon related to this promotion - And there should be coupon with code "SANTA2016" + And there should be a "Christmas sale" promotion with a coupon code "SANTA2016" diff --git a/features/promotion/managing_coupons/coupon_unique_code_validation.feature b/features/promotion/managing_coupons/coupon_unique_code_validation.feature index 4f2a1d82bf3..9331d10ee5e 100644 --- a/features/promotion/managing_coupons/coupon_unique_code_validation.feature +++ b/features/promotion/managing_coupons/coupon_unique_code_validation.feature @@ -9,7 +9,7 @@ Feature: Coupon unique code validation And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add coupon with taken code When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" diff --git a/features/promotion/managing_coupons/coupon_validation.feature b/features/promotion/managing_coupons/coupon_validation.feature index 282d5bd5e1d..d1c42498094 100644 --- a/features/promotion/managing_coupons/coupon_validation.feature +++ b/features/promotion/managing_coupons/coupon_validation.feature @@ -10,7 +10,7 @@ Feature: Coupon validation And it is coupon based promotion And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new coupon without specifying its code When I want to create a new coupon for this promotion And I do not specify its code @@ -19,9 +19,9 @@ Feature: Coupon validation And I make it valid until "26.03.2017" And I try to add it Then I should be notified that code is required - And there should be 0 coupon related to this promotion + And there should be 0 coupons related to this promotion - @ui + @ui @api Scenario: Trying to add a new coupon with usage limit below one When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" @@ -30,9 +30,9 @@ Feature: Coupon validation And I make it valid until "26.03.2017" And I try to add it Then I should be notified that coupon usage limit must be at least one - And there should be 0 coupon related to this promotion + And there should be 0 coupons related to this promotion - @ui + @ui @api Scenario: Trying to add a new coupon with per customer usage limit below one When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" @@ -41,4 +41,27 @@ Feature: Coupon validation And I make it valid until "26.03.2017" And I try to add it Then I should be notified that coupon usage limit per customer must be at least one - And there should be 0 coupon related to this promotion + And there should be 0 coupons related to this promotion + + @api @no-ui + Scenario: Trying to add a new coupon with no promotion + When I want to create a new coupon + And I specify its code as "RANDOM" + And I limit its usage to 30 times + And I limit its per customer usage to 3 times + And I make it valid until "26.03.2017" + And I try to add it + Then I should be notified that promotion is required + And there should be no coupon with code "RANDOM" + + @api @no-ui + Scenario: Trying to add a new coupon for a non-coupon based promotion + Given there is a promotion "Flash sale" + When I want to create a new coupon for this promotion + And I specify its code as "FAST-50" + And I limit its usage to 50 times + And I limit its per customer usage to 1 time + And I make it valid until "26.03.2017" + And I try to add it + Then I should be notified that only coupon based promotions can have coupons + And there should be 0 coupons related to this promotion diff --git a/features/promotion/managing_coupons/deleting_coupon.feature b/features/promotion/managing_coupons/deleting_coupon.feature index efe196ac6f3..08a737d4102 100644 --- a/features/promotion/managing_coupons/deleting_coupon.feature +++ b/features/promotion/managing_coupons/deleting_coupon.feature @@ -9,7 +9,7 @@ Feature: Deleting a coupon And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @domain @ui + @domain @ui @api Scenario: Deleted coupon should disappear from the registry When I delete "SANTA2016" coupon related to this promotion Then I should be notified that it has been successfully deleted diff --git a/features/promotion/managing_coupons/editing_coupon.feature b/features/promotion/managing_coupons/editing_coupon.feature index 0bbe347fdd5..c1e4afe187c 100644 --- a/features/promotion/managing_coupons/editing_coupon.feature +++ b/features/promotion/managing_coupons/editing_coupon.feature @@ -9,15 +9,15 @@ Feature: Editing promotion coupon And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui + @ui @api Scenario: Changing coupon expires date When I want to modify the "SANTA2016" coupon for this promotion - And I change expires date to "21.05.2019" + And I change its expiration date to "21.05.2019" And I save my changes Then I should be notified that it has been successfully edited And this coupon should be valid until "21.05.2019" - @ui + @ui @api Scenario: Changing coupons usage limit When I want to modify the "SANTA2016" coupon for this promotion And I change its usage limit to 50 @@ -25,7 +25,7 @@ Feature: Editing promotion coupon Then I should be notified that it has been successfully edited And this coupon should have 50 usage limit - @ui + @ui @api Scenario: Changing coupons per customer usage limit When I want to modify the "SANTA2016" coupon for this promotion And I change its per customer usage limit to 20 @@ -33,7 +33,15 @@ Feature: Editing promotion coupon Then I should be notified that it has been successfully edited And this coupon should have 20 per customer usage limit - @ui - Scenario: Seeing a disabled code field when editing a coupon + @ui @api + Scenario: Changing whether it can be reused from cancelled orders When I want to modify the "SANTA2016" coupon for this promotion - Then the code field should be disabled + And I make it not reusable from cancelled orders + And I save my changes + Then I should be notified that it has been successfully edited + And this coupon should not be reusable from cancelled orders + + @ui @api + Scenario: Being unable to change code of promotion coupon + When I want to modify the "SANTA2016" coupon for this promotion + Then I should not be able to edit its code diff --git a/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature b/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature index 527ffe64b53..77d37799c1d 100644 --- a/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature +++ b/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature @@ -15,7 +15,7 @@ Feature: Not being able to delete a coupon which is in use And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @domain @ui + @domain @ui @api Scenario: Being unable to delete a used coupon When I try to delete "SANTA2016" coupon related to this promotion Then I should be notified that it is in use and cannot be deleted diff --git a/features/promotion/managing_coupons/sorting_coupons.feature b/features/promotion/managing_coupons/sorting_coupons.feature index b3c00f78099..e1eb8ba9d76 100644 --- a/features/promotion/managing_coupons/sorting_coupons.feature +++ b/features/promotion/managing_coupons/sorting_coupons.feature @@ -19,69 +19,69 @@ Feature: Sorting listed coupons And this coupon expires on "20-02-2023" And I am logged in as an administrator - @ui + @ui @api Scenario: Coupons are sorted by descending number of uses by default When I want to view all coupons of this promotion Then I should see 3 coupons on the list And the first coupon should have code "Y" - @ui + @ui @api Scenario: Changing the number of uses sorting order to ascending Given I am browsing coupons of this promotion When I sort coupons by ascending number of uses Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui + @ui @api Scenario: Sorting coupons by code in descending order Given I am browsing coupons of this promotion When I sort coupons by descending code Then I should see 3 coupons on the list And the first coupon should have code "Z" - @ui + @ui @api Scenario: Sorting coupons by code in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending code Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit in descending order Given I am browsing coupons of this promotion When I sort coupons by descending usage limit Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending usage limit Then I should see 3 coupons on the list And the first coupon should have code "Z" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit per customer in descending order Given I am browsing coupons of this promotion When I sort coupons by descending usage limit per customer Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit per customer in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending usage limit per customer Then I should see 3 coupons on the list And the first coupon should have code "Y" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by expiration date in descending order Given I am browsing coupons of this promotion When I sort coupons by descending expiration date Then I should see 3 coupons on the list And the first coupon should have code "Z" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by expiration date in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending expiration date diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php new file mode 100644 index 00000000000..d13e56e2e7a --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php @@ -0,0 +1,466 @@ +client->index(Resources::PROMOTION_COUPONS); + $this->client->addFilter( + 'promotion', + $this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'), + ); + $this->client->filter(); + } + + /** + * @When I want to create a new coupon + */ + public function iWantToCreateANewCoupon(): void + { + $this->client->buildCreateRequest(Resources::PROMOTION_COUPONS); + } + + /** + * @When /^I want to create a new coupon for (this promotion)$/ + */ + public function iWantToCreateANewCouponForPromotion(PromotionInterface $promotion): void + { + $this->client->buildCreateRequest(Resources::PROMOTION_COUPONS); + $this->client->addRequestData( + 'promotion', + $this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'), + ); + } + + /** + * @When I want to modify the :coupon coupon for this promotion + */ + public function iWantToModifyTheCouponOfThisPromotion(PromotionCouponInterface $coupon): void + { + $this->client->buildUpdateRequest(Resources::PROMOTION_COUPONS, $coupon->getCode()); + } + + /** + * @When I (try to) delete :coupon coupon related to this promotion + */ + public function iDeleteCouponRelatedToThisPromotion(PromotionCouponInterface $coupon): void + { + $this->client->delete(Resources::PROMOTION_COUPONS, $coupon->getCode()); + } + + /** + * @When I specify its code as :code + */ + public function iSpecifyItsCodeAs(string $code): void + { + $this->client->addRequestData('code', $code); + } + + /** + * @When I limit its usage to :times time(s) + * @When I change its usage limit to :times + */ + public function iLimitItsUsageToTimes(int $times): void + { + $this->client->addRequestData('usageLimit', $times); + } + + /** + * @When I limit its per customer usage to :times time(s) + * @When I change its per customer usage limit to :times + */ + public function iLimitItsPerCustomerUsageToTimes(int $times): void + { + $this->client->addRequestData('perCustomerUsageLimit', $times); + } + + /** + * @When I make it valid until :date + * @When I change its expiration date to :date + */ + public function iMakeItValidUntil(\DateTime $date): void + { + $this->client->addRequestData('expiresAt', $date->format('d-m-Y')); + } + + /** + * @When I make it not reusable from cancelled orders + */ + public function iMakeItNotReusableFromCancelledOrders(): void + { + $this->client->addRequestData('reusableFromCancelledOrders', false); + } + + /** + * @When I do not specify its :field + */ + public function iDoNotSpecifyIts(): void + { + // Intentionally left blank + } + + /** + * @When I (try to) add it + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When /^I sort coupons by (ascending|descending) number of uses$/ + */ + public function iSortCouponsByNumberOfUses(string $order): void + { + $this->sortBy($order, 'used'); + } + + /** + * @When /^I sort coupons by (ascending|descending) code$/ + */ + public function iSortCouponsByCode(string $order): void + { + $this->sortBy($order, 'code'); + } + + /** + * @When /^I sort coupons by (ascending|descending) usage limit$/ + */ + public function iSortCouponsByUsageLimit(string $order): void + { + $this->sortBy($order, 'usageLimit'); + } + + /** + * @When /^I sort coupons by (ascending|descending) usage limit per customer$/ + */ + public function iSortCouponsByPerCustomerUsageLimit(string $order): void + { + $this->sortBy($order, 'perCustomerUsageLimit'); + } + + /** + * @When /^I sort coupons by (ascending|descending) expiration date$/ + */ + public function iSortCouponsByExpirationDate(string $order): void + { + $this->sortBy($order, 'expiresAt'); + } + + /** + * @Then /^there should(?:| still) be (\d+) coupons? related to (this promotion)$/ + */ + public function thereShouldBeCountCouponsRelatedToThisPromotion(int $count, PromotionInterface $promotion): void + { + $coupons = $this->responseChecker->getCollection( + $this->client->subResourceIndex(Resources::PROMOTIONS, Resources::PROMOTION_COUPONS, $promotion->getCode()), + ); + Assert::same(count($coupons), $count); + } + + /** + * @Then there should be a :promotion promotion with a coupon code :code + */ + public function thereShouldBeACouponWithCode(PromotionInterface $promotion, string $code): void + { + Assert::true($this->responseChecker->hasItemWithValue( + $this->client->subResourceIndex(Resources::PROMOTIONS, Resources::PROMOTION_COUPONS, $promotion->getCode()), + 'code', + $code, + )); + } + + /** + * @Then there should be no coupon with code :code + */ + public function thereShouldBeNoCouponWithCode(string $code): void + { + Assert::false($this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PROMOTION_COUPONS), + 'code', + $code, + )); + } + + /** + * @Then I should see :count coupons on the list + */ + public function iShouldSeeCountCouponsOnTheList(int $count): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then the first coupon should have code :code + */ + public function theFirstCouponShouldHaveCode(string $code): void + { + Assert::true($this->responseChecker->hasItemOnPositionWithValue( + $this->client->getLastResponse(), + 0, + 'code', + $code, + )); + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Promotion coupon could not be created', + ); + } + + /** + * @Then this coupon should be valid until :date + */ + public function thisCouponShouldBeValidUntil(\DateTime $date): void + { + $actualDate = \DateTime::createFromFormat( + 'Y-m-d h:i:s', + $this->responseChecker->getValue($this->client->getLastResponse(), 'expiresAt'), + ); + + Assert::same( + $actualDate->format('Y-m-d'), + $date->format('Y-m-d'), + ); + } + + /** + * @Then this coupon should have :limit usage limit + */ + public function thisCouponShouldHaveUsageLimit(int $limit): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'usageLimit'), + $limit, + ); + } + + /** + * @Then this coupon should have :limit per customer usage limit + */ + public function thisCouponShouldHavePerCustomerUsageLimit(int $limit): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'perCustomerUsageLimit'), + $limit, + ); + } + + /** + * @Then this coupon should not be reusable from cancelled orders + */ + public function thisCouponShouldNotBeReusableFromCancelledOrders(): void + { + Assert::false($this->responseChecker->getValue( + $this->client->getLastResponse(), + 'reusableFromCancelledOrders', + )); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + Assert::true( + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()), + 'Promotion coupon could not be deleted', + ); + } + + /** + * @Then /^(this coupon) should no longer exist in the coupon registry$/ + */ + public function couponShouldNotExistInTheRegistry(PromotionCouponInterface $coupon): void + { + Assert::false($this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PROMOTION_COUPONS), + 'code', + $coupon->getCode(), + )); + } + + /** + * @Then /^(this coupon) should still exist in the registry$/ + */ + public function couponShouldStillExistInTheRegistry(PromotionCouponInterface $coupon): void + { + Assert::true($this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PROMOTION_COUPONS), + 'code', + $coupon->getCode(), + )); + } + + /** + * @Then /^there should still be only one coupon with code "([^"]+)" related to (this promotion)$/ + */ + public function thereShouldStillBeOnlyOneCouponWithCodeRelatedTo(string $code, PromotionInterface $promotion): void + { + $coupons = $this->responseChecker->getCollectionItemsWithValue( + $this->client->subResourceIndex(Resources::PROMOTIONS, Resources::PROMOTION_COUPONS, $promotion->getCode()), + 'code', + $code + ); + + Assert::count($coupons, 1); + } + + /** + * @Then I should be notified that it is in use and cannot be deleted + */ + public function iShouldBeNotifiedThatItIsInUseAndCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete, the promotion coupon is in use.', + ); + } + + /** + * @Then I should be notified that code is required + */ + public function iShouldBeNotifiedThatCodeIsRequired(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same($this->responseChecker->getError($response), 'code: Please enter coupon code.'); + } + + /** + * @Then I should be notified that coupon usage limit must be at least one + */ + public function iShouldBeNotifiedThatCouponUsageLimitMustBeAtLeastOne(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'usageLimit: Coupon usage limit must be at least 1.', + ); + } + + /** + * @Then I should be notified that coupon usage limit per customer must be at least one + */ + public function iShouldBeNotifiedThatCouponUsageLimitPerCustomerMustBeAtLeastOne(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'perCustomerUsageLimit: Coupon usage limit per customer must be at least 1.', + ); + } + + /** + * @Then I should be notified that promotion is required + */ + public function iShouldBeNotifiedThatPromotionIsRequired(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'promotion: Please provide a promotion for this coupon.', + ); + } + + /** + * @Then I should be notified that only coupon based promotions can have coupons + */ + public function iShouldBeNotifiedThatOnlyCouponBasedPromotionsCanHaveCoupons(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'promotion: Only coupon based promotions can have coupons.', + ); + } + + /** + * @Then I should be notified that coupon with this code already exists + */ + public function iShouldBeNotifiedThatCouponWithThisCodeAlreadyExists(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'code: This coupon already exists.', + ); + } + + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + private function sortBy(string $order, string $field): void + { + $this->client->sort([$field => str_starts_with($order, 'de') ? 'desc' : 'asc']); + } +} diff --git a/src/Sylius/Behat/Context/Api/Resources.php b/src/Sylius/Behat/Context/Api/Resources.php index edd14cef201..b6874650827 100644 --- a/src/Sylius/Behat/Context/Api/Resources.php +++ b/src/Sylius/Behat/Context/Api/Resources.php @@ -71,6 +71,8 @@ final class Resources public const PROMOTIONS = 'promotions'; + public const PROMOTION_COUPONS = 'promotion-coupons'; + public const PROVINCES = 'provinces'; public const SHIPMENTS = 'shipments'; diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php index d4373f32085..c3977d7783e 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php @@ -98,7 +98,7 @@ public function specifySuffixAs(string $suffix): void } /** - * @When /^I limit generated coupons usage to (\d+) times$/ + * @When /^I limit generated coupons usage to (\d+) times?$/ */ public function iSetGeneratedCouponsUsageLimitTo(int $limit) { @@ -123,7 +123,7 @@ public function iSpecifyItsCodeAs($code = null) } /** - * @When I limit its usage to :limit times + * @When I limit its usage to :limit time(s) */ public function iLimitItsUsageLimitTo(int $limit) { @@ -149,7 +149,7 @@ public function iSpecifyItsAmountAs(?int $amount = null): void } /** - * @When /^I limit its per customer usage to ([^"]+) times$/ + * @When /^I limit its per customer usage to ([^"]+) times?$/ */ public function iLimitItsPerCustomerUsageLimitTo(int $limit) { @@ -164,6 +164,14 @@ public function iChangeItsPerCustomerUsageLimitTo(int $limit) $this->updatePage->setCustomerUsageLimit($limit); } + /** + * @When I make it not reusable from cancelled orders + */ + public function iMakeItReusableFromCancelledOrders(): void + { + $this->updatePage->toggleReusableFromCancelledOrders(false); + } + /** * @When I make it valid until :date */ @@ -173,9 +181,9 @@ public function iMakeItValidUntil(\DateTimeInterface $date) } /** - * @When I change expires date to :date + * @When I change its expiration date to :date */ - public function iChangeExpiresDateTo(\DateTimeInterface $date) + public function iChangeItsExpirationDateTo(\DateTimeInterface $date) { $this->updatePage->setExpiresAt($date); } @@ -275,9 +283,7 @@ public function iSortCouponsByExpirationDate(string $order): void } /** - * @Then /^there should be (0|1) coupon related to (this promotion)$/ - * @Then /^there should be (\b(?![01]\b)\d{1,9}\b) coupons related to (this promotion)$/ - * @Then /^there should still be (\d+) coupons related to (this promotion)$/ + * @Then /^there should(?:| still) be (\d+) coupons? related to (this promotion)$/ */ public function thereShouldBeCouponRelatedTo(int $number, PromotionInterface $promotion): void { @@ -315,9 +321,10 @@ public function iShouldSeeASingleCouponInTheList(): void } /** - * @Then /^there should be coupon with code "([^"]+)"$/ + * @Then there should be a coupon with code :code + * @Then there should be a :promotion promotion with a coupon code :code */ - public function thereShouldBeCouponWithCode($code) + public function thereShouldBeCouponWithCode(string $code): void { Assert::true($this->indexPage->isSingleResourceOnPage(['code' => $code])); } @@ -325,9 +332,9 @@ public function thereShouldBeCouponWithCode($code) /** * @Then this coupon should be valid until :date */ - public function thisCouponShouldBeValidUntil(\DateTimeInterface $date) + public function thisCouponShouldBeValidUntil(\DateTime $date) { - Assert::true($this->indexPage->isSingleResourceOnPage(['expiresAt' => date('d-m-Y', $date->getTimestamp())])); + Assert::true($this->indexPage->isSingleResourceOnPage(['expiresAt' => $date->format('d-m-Y')])); } /** @@ -355,9 +362,20 @@ public function thisCouponShouldHavePerCustomerUsageLimit($limit) } /** + * @Then /^(this coupon) should not be reusable from cancelled orders$/ + */ + public function thisCouponShouldBeReusableFromCancelledOrders(PromotionCouponInterface $coupon): void + { + $this->updatePage->open(['id' => $coupon->getId(), 'promotionId' => $coupon->getPromotion()->getId()]); + + Assert::false($this->updatePage->isReusableFromCancelledOrders()); + } + + /** + * @Then I should not be able to edit its code * @Then the code field should be disabled */ - public function theCodeFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsCode(): void { Assert::true($this->updatePage->isCodeDisabled()); } diff --git a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php index 73ebd9ae60d..3cc55455401 100644 --- a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php +++ b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php @@ -16,6 +16,7 @@ use Behat\Mink\Element\NodeElement; use Sylius\Behat\Behaviour\ChecksCodeImmutability; use Sylius\Behat\Page\Admin\Crud\UpdatePage as BaseUpdatePage; +use Webmozart\Assert\Assert; class UpdatePage extends BaseUpdatePage implements UpdatePageInterface { @@ -38,6 +39,20 @@ public function setUsageLimit(string $limit): void $this->getDocument()->fillField('Usage limit', $limit); } + public function isReusableFromCancelledOrders(): bool + { + return $this->getElement('reusable_from_cancelled_orders')->isChecked(); + } + + public function toggleReusableFromCancelledOrders(bool $reusable): void + { + $toggle = $this->getElement('reusable_from_cancelled_orders'); + + Assert::notSame($toggle->isChecked(), $reusable); + + $reusable ? $toggle->check() : $toggle->uncheck(); + } + protected function getCodeElement(): NodeElement { return $this->getElement('code'); @@ -50,6 +65,7 @@ protected function getDefinedElements(): array 'expires_at' => '#sylius_promotion_coupon_expiresAt', 'usage_limit' => '#sylius_promotion_coupon_usageLimit', 'per_customer_usage_limit' => '#sylius_promotion_coupon_perCustomerUsageLimit', + 'reusable_from_cancelled_orders' => '#sylius_promotion_coupon_reusableFromCancelledOrders', ]); } } diff --git a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php index 9cfc8f1113b..9376fee036b 100644 --- a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php @@ -24,4 +24,8 @@ public function setCustomerUsageLimit(int $limit): void; public function setExpiresAt(\DateTimeInterface $date): void; public function setUsageLimit(string $limit): void; + + public function isReusableFromCancelledOrders(): bool; + + public function toggleReusableFromCancelledOrders(bool $reusable): 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 a18420f1509..2395287ecaa 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -290,5 +290,11 @@ + + + + + + diff --git a/src/Sylius/Behat/Resources/config/suites.yml b/src/Sylius/Behat/Resources/config/suites.yml index ce0c23d6fd4..92c39b2c0db 100644 --- a/src/Sylius/Behat/Resources/config/suites.yml +++ b/src/Sylius/Behat/Resources/config/suites.yml @@ -49,6 +49,7 @@ imports: - suites/api/promotion/applying_promotion_coupon.yml - suites/api/promotion/applying_promotion_rules.yml - suites/api/promotion/managing_catalog_promotions.yml + - suites/api/promotion/managing_promotion_coupons.yml - suites/api/promotion/managing_promotions.yml - suites/api/promotion/receiving_discount.yml - suites/api/promotion/removing_catalog_promotions.yml diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotion_coupons.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotion_coupons.yml new file mode 100644 index 00000000000..37509d15ec7 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotion_coupons.yml @@ -0,0 +1,35 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_promotion_coupons: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.coupon + - sylius.behat.context.transform.address + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.date_time + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.promotion + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.order + - sylius.behat.context.setup.admin_api_security + + - sylius.behat.context.api.admin.managing_promotion_coupons + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@managing_promotion_coupons&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml b/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml index d377ae99897..460c94b06b2 100644 --- a/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml +++ b/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml @@ -28,6 +28,5 @@ default: - sylius.behat.context.domain.notification - sylius.behat.context.domain.security - filters: tags: "@managing_promotion_coupons&&@domain" diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionCouponDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionCouponDataPersister.php new file mode 100644 index 00000000000..efda67fdafd --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionCouponDataPersister.php @@ -0,0 +1,50 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof PromotionCouponInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []): void + { + try { + $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new PromotionCouponCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/PromotionCouponCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/PromotionCouponCannotBeRemoved.php new file mode 100644 index 00000000000..21d84fe82b7 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/PromotionCouponCannotBeRemoved.php @@ -0,0 +1,26 @@ + $properties */ + public function __construct( + private IriConverterInterface $iriConverter, + ManagerRegistry $managerRegistry, + ?RequestStack $requestStack = null, + LoggerInterface $logger = null, + array $properties = null, + NameConverterInterface $nameConverter = null, + ) { + parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter); + } + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + string $operationName = null, + ): void { + if (self::PROPERTY !== $property) { + return; + } + + $promotion = $this->iriConverter->getResourceFromIri($value); + + $parameterName = $queryNameGenerator->generateParameterName(':promotion'); + $promotionJoinAlias = $queryNameGenerator->generateJoinAlias('promotion'); + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder + ->innerJoin( + sprintf('%s.promotion', $rootAlias), + $promotionJoinAlias, + Join::WITH, + $queryBuilder->expr()->eq(sprintf('%s.id', $promotionJoinAlias), $parameterName), + ) + ->setParameter($parameterName, $promotion) + ; + } + + /** @return array */ + public function getDescription(string $resourceClass): array + { + return [ + self::PROPERTY => [ + 'type' => 'string', + 'required' => false, + 'property' => null, + 'description' => 'Get a collection of promotion coupons for promotion', + 'schema' => [ + 'type' => 'string', + ], + ], + ]; + } +} 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 4e76202e919..71c5ef7f2a7 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml @@ -133,6 +133,13 @@ Example configuration for `order_fixed_discount` action type: + + + GET + /admin/promotions/{code}/promotion-coupons + + + @@ -144,7 +151,9 @@ Example configuration for `order_fixed_discount` action type: - + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml index cb50a6781c6..21cb126e1cf 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml @@ -18,7 +18,33 @@ admin - + sylius + + + + GET + + sylius.api.promotion_coupon_order_filter + Sylius\Bundle\ApiBundle\Filter\Doctrine\PromotionCouponPromotionFilter + + + admin:promotion_coupon:read + + + DESC + + + + + POST + + admin:promotion_coupon:read + + + admin:promotion_coupon:create + + + @@ -27,8 +53,39 @@ admin:promotion_coupon:read + + + PUT + + admin:promotion_coupon:read + + + admin:promotion_coupon:update + + + + + DELETE + - + + + + admin:promotion_coupon:read + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml index 42c3fbad41c..04acbb12754 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml @@ -61,6 +61,7 @@ api_platform: Sylius\Bundle\ApiBundle\Exception\ProductAttributeCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProductCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProductVariantCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\PromotionCouponCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\TaxonCannotBeRemoved: 422 diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionCoupon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionCoupon.xml new file mode 100644 index 00000000000..1783c72ef62 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionCoupon.xml @@ -0,0 +1,65 @@ + + + + + + + + admin:promotion_coupon:read + + + + admin:promotion_coupon:read + + + + admin:promotion_coupon:read + admin:promotion_coupon:create + + + + admin:promotion_coupon:read + admin:promotion_coupon:create + admin:promotion_coupon:update + + + + admin:promotion_coupon:read + admin:promotion_coupon:create + admin:promotion_coupon:update + + + + admin:promotion_coupon:read + + + + admin:promotion_coupon:read + admin:promotion_coupon:create + admin:promotion_coupon:update + + + + admin:promotion_coupon:read + admin:promotion_coupon:create + + + + admin:promotion_coupon:read + admin:promotion_coupon:create + admin:promotion_coupon:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml index 6a54784b8c0..bc5248e1185 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml @@ -85,6 +85,11 @@ + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml index c47196b10eb..e12e77a4368 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml @@ -273,5 +273,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionCouponDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionCouponDataPersisterSpec.php new file mode 100644 index 00000000000..d1e7742ad04 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionCouponDataPersisterSpec.php @@ -0,0 +1,66 @@ +beConstructedWith($dataPersister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_promotion_coupon(PromotionCouponInterface $coupon): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($coupon)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_promotion_coupon( + ContextAwareDataPersisterInterface $dataPersister, + PromotionCouponInterface $coupon, + ): void { + $dataPersister->persist($coupon, [])->shouldBeCalled(); + + $this->persist($coupon); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $dataPersister, + PromotionCouponInterface $coupon, + ): void { + $dataPersister->remove($coupon, [])->willThrow(ForeignKeyConstraintViolationException::class); + + $this->shouldThrow(PromotionCouponCannotBeRemoved::class)->during('remove', [$coupon]); + } + + function it_uses_inner_persister_to_remove_promotion_coupon( + ContextAwareDataPersisterInterface $dataPersister, + PromotionCouponInterface $coupon, + ): void { + $dataPersister->remove($coupon, [])->shouldBeCalled(); + + $this->remove($coupon); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml index 42f3083e7dd..716dfa57c9c 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml @@ -50,5 +50,9 @@ %sylius.promotion_rules% + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml index 91ed8fd9826..5b906d06b2f 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml @@ -18,6 +18,16 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml index 543d8f1f0e0..d2158aa2d13 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml @@ -47,6 +47,9 @@ sylius: regex: Coupon code can only be comprised of letters, numbers, dashes and underscores. unique: This coupon already exists. is_invalid: Coupon code is invalid. + promotion: + not_blank: Please provide a promotion for this coupon. + not_coupon_based: Only coupon based promotions can have coupons. usage_limit: min: Coupon usage limit must be at least {{ limit }}. promotion_coupon_generator_instruction: diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/Constraints/PromotionNotCouponBased.php b/src/Sylius/Bundle/PromotionBundle/Validator/Constraints/PromotionNotCouponBased.php new file mode 100644 index 00000000000..8f53f5abfad --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/Constraints/PromotionNotCouponBased.php @@ -0,0 +1,31 @@ +getPromotion(); + if (null === $promotion) { + return; + } + + if (!$promotion->isCouponBased()) { + $this->context + ->buildViolation($constraint->message) + ->atPath('promotion') + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php new file mode 100644 index 00000000000..b83b5229b07 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php @@ -0,0 +1,111 @@ +initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_does_nothing_when_value_is_null(ExecutionContextInterface $context): void + { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate(null, new PromotionNotCouponBased()); + } + + function it_throws_an_exception_when_constraint_is_not_promotion_not_coupon_based( + ExecutionContextInterface $context, + PromotionCouponInterface $coupon, + Constraint $constraint, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$coupon, $constraint]) + ; + } + + function it_throws_an_exception_when_value_is_not_a_coupon(ExecutionContextInterface $context): void + { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', [new \stdClass(), new PromotionNotCouponBased()]) + ; + } + + function it_does_nothing_when_coupon_has_no_promotion( + ExecutionContextInterface $context, + PromotionCouponInterface $coupon, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $coupon->getPromotion()->willReturn(null); + + $this->validate($coupon, new PromotionNotCouponBased()); + } + + function it_does_nothing_when_coupon_has_promotion_and_its_coupon_based( + ExecutionContextInterface $context, + PromotionCouponInterface $coupon, + PromotionInterface $promotion, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $promotion->isCouponBased()->willReturn(true); + $coupon->getPromotion()->willReturn($promotion); + + $this->validate($coupon, new PromotionNotCouponBased()); + } + + function it_adds_violation_when_coupon_has_promotion_but_its_not_coupon_based( + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + PromotionCouponInterface $coupon, + PromotionInterface $promotion, + ): void { + $constraint = new PromotionNotCouponBased(); + + $context->buildViolation($constraint->message)->willReturn($violationBuilder); + $violationBuilder->atPath('promotion')->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $promotion->isCouponBased()->willReturn(false); + $coupon->getPromotion()->willReturn($promotion); + + $this->validate($coupon, $constraint); + } +} diff --git a/tests/Api/Admin/PromotionCouponsTest.php b/tests/Api/Admin/PromotionCouponsTest.php new file mode 100644 index 00000000000..2396a8b8e62 --- /dev/null +++ b/tests/Api/Admin/PromotionCouponsTest.php @@ -0,0 +1,161 @@ +loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'promotion/promotion.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var PromotionCouponInterface $coupon */ + $coupon = $fixtures['promotion_1_off_coupon_1']; + + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/promotion-coupons/%s', $coupon->getCode()), + server: $header, + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion_coupon/get_promotion_coupon_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_gets_promotion_coupons(): void + { + $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'promotion/promotion.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request(method: 'GET', uri: '/api/v2/admin/promotion-coupons', server: $header); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion_coupon/get_promotion_coupons_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_creates_a_promotion_coupon(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'promotion/promotion.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var PromotionInterface $promotion */ + $promotion = $fixtures['promotion_1_off']; + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/promotion-coupons', + server: $header, + content: json_encode([ + 'code' => 'XYZ3', + 'usageLimit' => 100, + 'perCustomerUsageLimit' => 3, + 'reusableFromCancelledOrders' => false, + 'expiresAt' => '23-12-2023', + 'promotion' => 'api/v2/admin/promotions/' . $promotion->getCode(), + ], JSON_THROW_ON_ERROR) + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion_coupon/post_promotion_coupon_response', + Response::HTTP_CREATED, + ); + } + + /** @test */ + public function it_updates_a_promotion_coupon(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'promotion/promotion.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var PromotionCouponInterface $coupon */ + $coupon = $fixtures['promotion_1_off_coupon_1']; + + $this->client->request( + method: 'PUT', + uri: '/api/v2/admin/promotion-coupons/' . $coupon->getCode(), + server: $header, + content: json_encode([ + 'usageLimit' => 1000, + 'perCustomerUsageLimit' => 5, + 'reusableFromCancelledOrders' => false, + 'expiresAt' => '2020-01-01 12:00:00', + ], JSON_THROW_ON_ERROR) + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion_coupon/put_promotion_coupon_response', + Response::HTTP_OK, + ); + } + + /** @test */ + public function it_removes_a_promotion_coupon(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'promotion/promotion.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var PromotionCouponInterface $coupon */ + $coupon = $fixtures['promotion_1_off_coupon_1']; + + $this->client->request( + method: 'DELETE', + uri: '/api/v2/admin/promotion-coupons/' . $coupon->getCode(), + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NO_CONTENT); + } + + /** @test */ + public function it_does_not_delete_the_promotion_coupon_in_use(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'promotion/promotion.yaml', + 'promotion/promotion_order.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var PromotionCouponInterface $coupon */ + $coupon = $fixtures['promotion_1_off_coupon_1']; + + $this->client->request( + method: 'DELETE', + uri: '/api/v2/admin/promotion-coupons/' . $coupon->getCode(), + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); + } +} diff --git a/tests/Api/Admin/PromotionsTest.php b/tests/Api/Admin/PromotionsTest.php index b86f1e8dfde..c3585c04b15 100644 --- a/tests/Api/Admin/PromotionsTest.php +++ b/tests/Api/Admin/PromotionsTest.php @@ -72,6 +72,28 @@ public function it_gets_promotions(): void ); } + /** @test */ + public function it_gets_promotion_coupons(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'promotion/promotion.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var PromotionInterface $promotion */ + $promotion = $fixtures['promotion_1_off']; + + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/promotions/%s/promotion-coupons', $promotion->getCode()), + server: $header + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/promotion/get_promotion_coupons_response', + Response::HTTP_OK, + ); + } + /** @test */ public function it_creates_promotion(): void { diff --git a/tests/Api/DataFixtures/ORM/promotion/promotion.yaml b/tests/Api/DataFixtures/ORM/promotion/promotion.yaml index 7749bac08b5..bb4496f4ba2 100644 --- a/tests/Api/DataFixtures/ORM/promotion/promotion.yaml +++ b/tests/Api/DataFixtures/ORM/promotion/promotion.yaml @@ -33,3 +33,20 @@ Sylius\Component\Promotion\Model\PromotionTranslation: locale: 'en_US' label: '1$ off every item!' translatable: '@promotion_1_off' + +Sylius\Component\Core\Model\PromotionCoupon: + promotion_1_off_coupon_1: + code: 'XYZ1' + usageLimit: 2 + perCustomerUsageLimit: 1 + used: 1 + reusableFromCancelledOrders: true + promotion: '@promotion_1_off' + expiresAt: null + promotion_1_off_coupon_2: + code: 'XYZ2' + usageLimit: null + perCustomerUsageLimit: null + reusableFromCancelledOrders: false + promotion: '@promotion_1_off' + expiresAt: diff --git a/tests/Api/DataFixtures/ORM/promotion/promotion_order.yaml b/tests/Api/DataFixtures/ORM/promotion/promotion_order.yaml index 6a7fd111ec0..c920d549528 100644 --- a/tests/Api/DataFixtures/ORM/promotion/promotion_order.yaml +++ b/tests/Api/DataFixtures/ORM/promotion/promotion_order.yaml @@ -6,3 +6,4 @@ Sylius\Component\Core\Model\Order: state: "new" tokenValue: "token" promotions: ['@promotion_50_off'] + promotionCoupon: '@promotion_1_off_coupon_1' diff --git a/tests/Api/Responses/Expected/admin/promotion/get_promotion_coupons_response.json b/tests/Api/Responses/Expected/admin/promotion/get_promotion_coupons_response.json new file mode 100644 index 00000000000..cd555764d0b --- /dev/null +++ b/tests/Api/Responses/Expected/admin/promotion/get_promotion_coupons_response.json @@ -0,0 +1,34 @@ +{ + "@context": "\/api\/v2\/contexts\/PromotionCoupon", + "@id": "\/api\/v2\/admin\/promotions/dollar_off/promotion-coupons", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ1", + "@type": "PromotionCoupon", + "perCustomerUsageLimit": 1, + "reusableFromCancelledOrders": true, + "code": "XYZ1", + "usageLimit": 2, + "used": 1, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "expiresAt": null, + "createdAt": @date@, + "updatedAt": @date@ + }, + { + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ2", + "@type": "PromotionCoupon", + "perCustomerUsageLimit": null, + "reusableFromCancelledOrders": false, + "code": "XYZ2", + "usageLimit": null, + "used": 0, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "expiresAt": @date@, + "createdAt": @date@, + "updatedAt": @date@ + } + ], + "hydra:totalItems": 2 +} diff --git a/tests/Api/Responses/Expected/admin/promotion/get_promotion_response.json b/tests/Api/Responses/Expected/admin/promotion/get_promotion_response.json index 59910304922..457730d357d 100644 --- a/tests/Api/Responses/Expected/admin/promotion/get_promotion_response.json +++ b/tests/Api/Responses/Expected/admin/promotion/get_promotion_response.json @@ -6,7 +6,9 @@ "code": "50_off", "name": "50% Off on your first order", "description": "Get 50% off of your first purchase", - "channels": ["\/api\/v2\/admin\/channels\/MOBILE"], + "channels": [ + "\/api\/v2\/admin\/channels\/MOBILE" + ], "priority": 1, "exclusive": true, "appliesToDiscounted": false, diff --git a/tests/Api/Responses/Expected/admin/promotion/get_promotions_response.json b/tests/Api/Responses/Expected/admin/promotion/get_promotions_response.json index 42e62fd1705..1b267930390 100644 --- a/tests/Api/Responses/Expected/admin/promotion/get_promotions_response.json +++ b/tests/Api/Responses/Expected/admin/promotion/get_promotions_response.json @@ -10,7 +10,10 @@ "code": "dollar_off", "name": "1 dollar off each item", "description": "Get 1 dollar off every item when buying more than 10", - "channels": ["\/api\/v2\/admin\/channels\/WEB", "\/api\/v2\/admin\/channels\/MOBILE"], + "channels": [ + "\/api\/v2\/admin\/channels\/WEB", + "\/api\/v2\/admin\/channels\/MOBILE" + ], "priority": 2, "exclusive": false, "appliesToDiscounted": true, @@ -19,7 +22,10 @@ "couponBased": true, "startsAt": null, "endsAt": null, - "coupons": [], + "coupons": [ + "\/api\/v2\/admin\/promotion-coupons\/XYZ1", + "\/api\/v2\/admin\/promotion-coupons\/XYZ2" + ], "rules": [], "actions": [], "createdAt": @date@, @@ -40,7 +46,9 @@ "code": "50_off", "name": "50% Off on your first order", "description": "Get 50% off of your first purchase", - "channels": ["\/api\/v2\/admin\/channels\/MOBILE"], + "channels": [ + "\/api\/v2\/admin\/channels\/MOBILE" + ], "priority": 1, "exclusive": true, "appliesToDiscounted": false, @@ -67,7 +75,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]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { diff --git a/tests/Api/Responses/Expected/admin/promotion_coupon/get_promotion_coupon_response.json b/tests/Api/Responses/Expected/admin/promotion_coupon/get_promotion_coupon_response.json new file mode 100644 index 00000000000..0356364fdc8 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/promotion_coupon/get_promotion_coupon_response.json @@ -0,0 +1,14 @@ +{ + "@context": "\/api\/v2\/contexts\/PromotionCoupon", + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ1", + "@type": "PromotionCoupon", + "code": "XYZ1", + "usageLimit": 2, + "perCustomerUsageLimit": 1, + "used": 1, + "reusableFromCancelledOrders": true, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "createdAt": @date@, + "updatedAt": @date@, + "expiresAt": null +} diff --git a/tests/Api/Responses/Expected/admin/promotion_coupon/get_promotion_coupons_response.json b/tests/Api/Responses/Expected/admin/promotion_coupon/get_promotion_coupons_response.json new file mode 100644 index 00000000000..237bffff856 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/promotion_coupon/get_promotion_coupons_response.json @@ -0,0 +1,77 @@ +{ + "@context": "\/api\/v2\/contexts\/PromotionCoupon", + "@id": "\/api\/v2\/admin\/promotion-coupons", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ1", + "@type": "PromotionCoupon", + "perCustomerUsageLimit": 1, + "reusableFromCancelledOrders": true, + "code": "XYZ1", + "usageLimit": 2, + "used": 1, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "expiresAt": null, + "createdAt": @date@, + "updatedAt": @date@ + }, + { + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ2", + "@type": "PromotionCoupon", + "perCustomerUsageLimit": null, + "reusableFromCancelledOrders": false, + "code": "XYZ2", + "usageLimit": null, + "used": 0, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "expiresAt": @date@, + "createdAt": @date@, + "updatedAt": @date@ + } + ], + "hydra:totalItems": 2, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "\/api\/v2\/admin\/promotion-coupons{?order[code],order[expiresAt],order[usageLimit],order[perCustomerUsageLimit],order[used],promotion}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "order[code]", + "property": "code", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[expiresAt]", + "property": "expiresAt", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[usageLimit]", + "property": "usageLimit", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[perCustomerUsageLimit]", + "property": "perCustomerUsageLimit", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[used]", + "property": "used", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "promotion", + "property": null, + "required": false + } + ] + } +} diff --git a/tests/Api/Responses/Expected/admin/promotion_coupon/post_promotion_coupon_response.json b/tests/Api/Responses/Expected/admin/promotion_coupon/post_promotion_coupon_response.json new file mode 100644 index 00000000000..8542e47a235 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/promotion_coupon/post_promotion_coupon_response.json @@ -0,0 +1,14 @@ +{ + "@context": "\/api\/v2\/contexts\/PromotionCoupon", + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ3", + "@type": "PromotionCoupon", + "code": "XYZ3", + "usageLimit": 100, + "perCustomerUsageLimit": 3, + "used": 0, + "reusableFromCancelledOrders": false, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "createdAt": @date@, + "updatedAt": @date@, + "expiresAt": @date@ +} diff --git a/tests/Api/Responses/Expected/admin/promotion_coupon/put_promotion_coupon_response.json b/tests/Api/Responses/Expected/admin/promotion_coupon/put_promotion_coupon_response.json new file mode 100644 index 00000000000..51cdf78a1bd --- /dev/null +++ b/tests/Api/Responses/Expected/admin/promotion_coupon/put_promotion_coupon_response.json @@ -0,0 +1,14 @@ +{ + "@context": "\/api\/v2\/contexts\/PromotionCoupon", + "@id": "\/api\/v2\/admin\/promotion-coupons\/XYZ1", + "@type": "PromotionCoupon", + "code": "XYZ1", + "usageLimit": 1000, + "perCustomerUsageLimit": 5, + "used": 1, + "reusableFromCancelledOrders": false, + "promotion": "\/api\/v2\/admin\/promotions\/dollar_off", + "createdAt": @date@, + "updatedAt": @date@, + "expiresAt": "2020-01-01 12:00:00" +}