From ba59fa7489a26c5299e27e3609e99f5ce3afb3a0 Mon Sep 17 00:00:00 2001 From: Wojdylak Date: Fri, 24 Nov 2023 16:41:10 +0100 Subject: [PATCH] [ApiBundle] Add promotion coupon endpoint as a subresource --- .../coupon_validation.feature | 2 +- .../Admin/ManagingPromotionCouponsContext.php | 33 ++++++++---------- .../config/api_resources/Promotion.xml | 11 +++++- .../config/api_resources/PromotionCoupon.xml | 8 +++++ .../PromotionNotCouponBasedValidator.php | 13 ++++--- .../PromotionNotCouponBasedValidatorSpec.php | 6 ++-- tests/Api/Admin/PromotionsTest.php | 22 ++++++++++++ .../get_promotion_coupons_response.json | 34 +++++++++++++++++++ 8 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 tests/Api/Responses/Expected/admin/promotion/get_promotion_coupons_response.json diff --git a/features/promotion/managing_coupons/coupon_validation.feature b/features/promotion/managing_coupons/coupon_validation.feature index 9bed0c47635c..d1c424980944 100644 --- a/features/promotion/managing_coupons/coupon_validation.feature +++ b/features/promotion/managing_coupons/coupon_validation.feature @@ -45,7 +45,7 @@ Feature: Coupon validation @api @no-ui Scenario: Trying to add a new coupon with no promotion - Given I want to create a new coupon + 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 diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php index 4462da9e9151..952319324e11 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php @@ -37,12 +37,11 @@ public function __construct( */ public function iWantToViewAllCouponsOfThisPromotion(PromotionInterface $promotion): void { - $this->client->index(Resources::PROMOTION_COUPONS); - $this->client->addFilter( - 'promotion', - $this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'), + $this->client->subResourceIndex( + Resources::PROMOTIONS, + Resources::PROMOTION_COUPONS, + $promotion->getCode() ); - $this->client->filter(); } /** @@ -66,7 +65,7 @@ public function iWantToCreateANewCouponForPromotion(PromotionInterface $promotio } /** - * @When /^I want to modify the ("[^"]+" coupon) for this promotion$/ + * @When I want to modify the :coupon coupon for this promotion */ public function iWantToModifyTheCouponOfThisPromotion(PromotionCouponInterface $coupon): void { @@ -74,8 +73,7 @@ public function iWantToModifyTheCouponOfThisPromotion(PromotionCouponInterface $ } /** - * @When /^I delete ("[^"]+" coupon) related to this promotion$/ - * @When /^I try to delete ("[^"]+" coupon) related to this promotion$/ + * @When I (try to) delete :coupon coupon related to this promotion */ public function iDeleteCouponRelatedToThisPromotion(PromotionCouponInterface $coupon): void { @@ -186,13 +184,7 @@ public function iSortCouponsByExpirationDate(string $order): void */ public function thereShouldBeCountCouponsRelatedToThisPromotion(int $count, PromotionInterface $promotion): void { - $coupons = $this->responseChecker->getCollectionItemsWithValue( - $this->client->index(Resources::PROMOTION_COUPONS), - 'promotion', - $this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'), - ); - - Assert::same(count($coupons), $count); + Assert::same(count($this->responseChecker->getCollection($this->client->getLastResponse())), $count); } /** @@ -201,7 +193,7 @@ public function thereShouldBeCountCouponsRelatedToThisPromotion(int $count, Prom public function thereShouldBeACouponWithCode(string $code): void { Assert::true($this->responseChecker->hasItemWithValue( - $this->client->index(Resources::PROMOTION_COUPONS), + $this->client->getLastResponse(), 'code', $code, )); @@ -340,10 +332,13 @@ public function couponShouldStillExistInTheRegistry(PromotionCouponInterface $co */ public function thereShouldStillBeOnlyOneCouponWithCodeRelatedTo(string $code, PromotionInterface $promotion): void { - Assert::count( - $this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::PROMOTION_COUPONS), 'code', $code), - 1 + $coupons = $this->responseChecker->getCollectionItemsWithValue( + $this->client->subResourceIndex(Resources::PROMOTIONS, Resources::PROMOTION_COUPONS, $promotion->getCode()), + 'code', + $code ); + + Assert::count($coupons, 1); } /** 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 113a170d5019..d64f15388aef 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml @@ -130,6 +130,13 @@ Example configuration for `order_fixed_discount` action type: + + + GET + /admin/promotions/{code}/promotion-coupons + + + @@ -141,7 +148,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 edb02c979d40..220fa2ef6fa2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml @@ -68,6 +68,14 @@ + + + + admin:promotion_coupon:read + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php index dd33bb9f959c..3020c4ecf08f 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php @@ -17,7 +17,8 @@ use Sylius\Component\Promotion\Model\PromotionCouponInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Webmozart\Assert\Assert; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; final class PromotionNotCouponBasedValidator extends ConstraintValidator { @@ -27,11 +28,13 @@ public function validate(mixed $value, Constraint $constraint): void return; } - /** @var PromotionNotCouponBased $constraint */ - Assert::isInstanceOf($constraint, PromotionNotCouponBased::class); + if (!$constraint instanceof PromotionNotCouponBased) { + throw new UnexpectedTypeException($constraint, PromotionNotCouponBased::class); + } - /** @var PromotionCouponInterface $value */ - Assert::isInstanceOf($value, PromotionCouponInterface::class); + if (!$value instanceof PromotionCouponInterface) { + throw new UnexpectedValueException($value, PromotionCouponInterface::class); + } $promotion = $value->getPromotion(); if (null === $promotion) { diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php index e8737e05f87a..b83b5229b07a 100644 --- a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php @@ -21,6 +21,8 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; final class PromotionNotCouponBasedValidatorSpec extends ObjectBehavior @@ -50,7 +52,7 @@ function it_throws_an_exception_when_constraint_is_not_promotion_not_coupon_base $context->buildViolation(Argument::any())->shouldNotBeCalled(); $this - ->shouldThrow(\InvalidArgumentException::class) + ->shouldThrow(UnexpectedTypeException::class) ->during('validate', [$coupon, $constraint]) ; } @@ -60,7 +62,7 @@ function it_throws_an_exception_when_value_is_not_a_coupon(ExecutionContextInter $context->buildViolation(Argument::any())->shouldNotBeCalled(); $this - ->shouldThrow(\InvalidArgumentException::class) + ->shouldThrow(UnexpectedValueException::class) ->during('validate', [new \stdClass(), new PromotionNotCouponBased()]) ; } diff --git a/tests/Api/Admin/PromotionsTest.php b/tests/Api/Admin/PromotionsTest.php index 1b4252a7c71e..06a32947dc89 100644 --- a/tests/Api/Admin/PromotionsTest.php +++ b/tests/Api/Admin/PromotionsTest.php @@ -61,6 +61,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/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 000000000000..cd555764d0b3 --- /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 +}