[ApiBundle] Add promotion coupon endpoint as a subresource

This commit is contained in:
Wojdylak 2023-11-24 19:16:07 +01:00
parent e6f4b2b344
commit c0ac149d0e
11 changed files with 104 additions and 26 deletions

View file

@ -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 a coupon with code "SANTA2016"
And there should be a "Christmas sale" promotion with a coupon code "SANTA2016"

View file

@ -13,4 +13,4 @@ Feature: Browsing promotion coupons
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 a coupon with code "SANTA2016"
And there should be a "Christmas sale" promotion with a coupon code "SANTA2016"

View file

@ -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

View file

@ -66,7 +66,7 @@ final class ManagingPromotionCouponsContext implements Context
}
/**
* @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 +74,7 @@ final class ManagingPromotionCouponsContext implements Context
}
/**
* @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,22 +185,19 @@ final class ManagingPromotionCouponsContext implements Context
*/
public function thereShouldBeCountCouponsRelatedToThisPromotion(int $count, PromotionInterface $promotion): void
{
$coupons = $this->responseChecker->getCollectionItemsWithValue(
$this->client->index(Resources::PROMOTION_COUPONS),
'promotion',
$this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'),
$coupons = $this->responseChecker->getCollection(
$this->client->subResourceIndex(Resources::PROMOTIONS, Resources::PROMOTION_COUPONS, $promotion->getCode()),
);
Assert::same(count($coupons), $count);
}
/**
* @Then there should be a coupon with code :code
* @Then there should be a :promotion promotion with a coupon code :code
*/
public function thereShouldBeACouponWithCode(string $code): void
public function thereShouldBeACouponWithCode(PromotionInterface $promotion, string $code): void
{
Assert::true($this->responseChecker->hasItemWithValue(
$this->client->index(Resources::PROMOTION_COUPONS),
$this->client->subResourceIndex(Resources::PROMOTIONS, Resources::PROMOTION_COUPONS, $promotion->getCode()),
'code',
$code,
));
@ -340,10 +336,13 @@ final class ManagingPromotionCouponsContext implements Context
*/
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);
}
/**

View file

@ -322,8 +322,9 @@ final class ManagingPromotionCouponsContext implements Context
/**
* @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]));
}

View file

@ -133,6 +133,13 @@ Example configuration for `order_fixed_discount` action type:
</itemOperation>
</itemOperations>
<subresourceOperations>
<subresourceOperation name="coupons_get_subresource">
<attribute name="method">GET</attribute>
<attribute name="path">/admin/promotions/{code}/promotion-coupons</attribute>
</subresourceOperation>
</subresourceOperations>
<property name="id" identifier="false" writable="false" />
<property name="code" identifier="true" readable="true" />
<property name="name" readable="true" />
@ -144,7 +151,9 @@ Example configuration for `order_fixed_discount` action type:
<property name="startsAt" readable="true" />
<property name="endsAt" readable="true" />
<property name="couponBased" readable="true" />
<property name="coupons" readable="true" />
<property name="coupons" readable="true" writable="true">
<subresource resourceClass="%sylius.model.promotion_coupon.class%" collection="true" />
</property>
<property name="rules" readable="true" />
<property name="actions" readable="true" />
<property name="channels" readable="true" />

View file

@ -68,6 +68,14 @@
</itemOperation>
</itemOperations>
<subresourceOperations>
<subresourceOperation name="api_promotions_coupons_get_subresource">
<attribute name="normalization_context">
<attribute name="groups">admin:promotion_coupon:read</attribute>
</attribute>
</subresourceOperation>
</subresourceOperations>
<property name="id" identifier="false" writable="false" />
<property name="code" identifier="true" readable="true" />
<property name="usageLimit" readable="true" />

View file

@ -17,7 +17,8 @@ use Sylius\Bundle\PromotionBundle\Validator\Constraints\PromotionNotCouponBased;
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 @@ final class PromotionNotCouponBasedValidator extends ConstraintValidator
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) {

View file

@ -21,6 +21,8 @@ use Sylius\Component\Promotion\Model\PromotionInterface;
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 @@ final class PromotionNotCouponBasedValidatorSpec extends ObjectBehavior
$context->buildViolation(Argument::any())->shouldNotBeCalled();
$this
->shouldThrow(\InvalidArgumentException::class)
->shouldThrow(UnexpectedTypeException::class)
->during('validate', [$coupon, $constraint])
;
}
@ -60,7 +62,7 @@ final class PromotionNotCouponBasedValidatorSpec extends ObjectBehavior
$context->buildViolation(Argument::any())->shouldNotBeCalled();
$this
->shouldThrow(\InvalidArgumentException::class)
->shouldThrow(UnexpectedValueException::class)
->during('validate', [new \stdClass(), new PromotionNotCouponBased()])
;
}

View file

@ -72,6 +72,28 @@ final class PromotionsTest extends JsonApiTestCase
);
}
/** @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
{

View file

@ -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
}