mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 09:02:12 +00:00
feature #15539 [API][Admin] Promotion coupons management(CRUD) (NoResponseMate, Wojdylak)
This PR was merged into the 1.13 branch. Discussion ---------- | Q | A | |-----------------|--------------------------------------------------------------| | Branch? | 1.13 | | Bug fix? | no | | New feature? | yes | | BC breaks? | no | | Deprecations? | no | | Related tickets | - | | License | MIT | Commits -------47153967eb[API][Admin] Browsing coupons41efb5f437[API][Admin] Sorting couponscb3770b564[API][Admin] Deleting coupons9b884466f4[API][Admin] Creating coupons54b87d0913[API][Admin] Updating couponsae13ab3969[API][Admin] Validating coupons854393e293Add coupons to payload in PromotionCouponsTest6d33557dc9Add api test for coupon_unique_code_validation.feature440a75ab83Resolve phpstan issuese6f4b2b344Add PromotionCouponCannotBeRemoved to code mappingc0ac149d0e[ApiBundle] Add promotion coupon endpoint as a subresource41ed0f33d5[ApiBundle] Add PromotionCouponPromotionFilter class08251e45c3[ApiBundle] Resolve phpstan issuese82780e1c0[ApiBundle] Add to test information abut PromotionCouponPromotionFilter
This commit is contained in:
commit
079766a2c2
44 changed files with 1588 additions and 53 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,466 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Behat\Context\Api\Admin;
|
||||
|
||||
use Behat\Behat\Context\Context;
|
||||
use Sylius\Behat\Client\ApiClientInterface;
|
||||
use Sylius\Behat\Client\ResponseCheckerInterface;
|
||||
use Sylius\Behat\Context\Api\Resources;
|
||||
use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface;
|
||||
use Sylius\Component\Core\Model\PromotionCouponInterface;
|
||||
use Sylius\Component\Core\Model\PromotionInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class ManagingPromotionCouponsContext implements Context
|
||||
{
|
||||
public function __construct(
|
||||
private ApiClientInterface $client,
|
||||
private ResponseCheckerInterface $responseChecker,
|
||||
private SectionAwareIriConverterInterface $sectionAwareIriConverter,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I am browsing coupons of (this promotion)$/
|
||||
* @When /^I want to view all coupons of (this promotion)$/
|
||||
*/
|
||||
public function iWantToViewAllCouponsOfThisPromotion(PromotionInterface $promotion): void
|
||||
{
|
||||
$this->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']);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
$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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
/**
|
||||
* @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 @@ final class ManagingPromotionCouponsContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Sylius\Behat\Page\Admin\PromotionCoupon;
|
|||
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 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface
|
|||
$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 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface
|
|||
'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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,8 @@ interface UpdatePageInterface extends BaseUpdatePageInterface
|
|||
public function setExpiresAt(\DateTimeInterface $date): void;
|
||||
|
||||
public function setUsageLimit(string $limit): void;
|
||||
|
||||
public function isReusableFromCancelledOrders(): bool;
|
||||
|
||||
public function toggleReusableFromCancelledOrders(bool $reusable): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,5 +290,11 @@
|
|||
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
|
||||
<argument type="service" id="sylius.behat.shared_storage" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.behat.context.api.admin.managing_promotion_coupons" class="Sylius\Behat\Context\Api\Admin\ManagingPromotionCouponsContext">
|
||||
<argument type="service" id="sylius.behat.api_platform_client.admin" />
|
||||
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
|
||||
<argument type="service" id="sylius.behat.section_iri_converter" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -28,6 +28,5 @@ default:
|
|||
- sylius.behat.context.domain.notification
|
||||
- sylius.behat.context.domain.security
|
||||
|
||||
|
||||
filters:
|
||||
tags: "@managing_promotion_coupons&&@domain"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\DataPersister;
|
||||
|
||||
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
|
||||
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
|
||||
use Sylius\Bundle\ApiBundle\Exception\PromotionCouponCannotBeRemoved;
|
||||
use Sylius\Component\Core\Model\PromotionCouponInterface;
|
||||
|
||||
/** @experimental */
|
||||
final class PromotionCouponDataPersister implements ContextAwareDataPersisterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ContextAwareDataPersisterInterface $decoratedDataPersister,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param array<array-key, mixed> $context */
|
||||
public function supports($data, array $context = []): bool
|
||||
{
|
||||
return $data instanceof PromotionCouponInterface;
|
||||
}
|
||||
|
||||
/** @param array<array-key, mixed> $context */
|
||||
public function persist($data, array $context = [])
|
||||
{
|
||||
return $this->decoratedDataPersister->persist($data, $context);
|
||||
}
|
||||
|
||||
/** @param array<array-key, mixed> $context */
|
||||
public function remove($data, array $context = []): void
|
||||
{
|
||||
try {
|
||||
$this->decoratedDataPersister->remove($data, $context);
|
||||
} catch (ForeignKeyConstraintViolationException) {
|
||||
throw new PromotionCouponCannotBeRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Exception;
|
||||
|
||||
/** @experimental */
|
||||
final class PromotionCouponCannotBeRemoved extends \RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'Cannot delete, the promotion coupon is in use.',
|
||||
int $code = 0,
|
||||
\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Filter\Doctrine;
|
||||
|
||||
use ApiPlatform\Api\IriConverterInterface;
|
||||
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
|
||||
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
|
||||
|
||||
final class PromotionCouponPromotionFilter extends AbstractContextAwareFilter
|
||||
{
|
||||
const PROPERTY = 'promotion';
|
||||
|
||||
/** @param array<array-key, mixed> $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<string, mixed> */
|
||||
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',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -18,7 +18,33 @@
|
|||
<resource class="%sylius.model.promotion_coupon.class%" shortName="PromotionCoupon">
|
||||
<attribute name="route_prefix">admin</attribute>
|
||||
|
||||
<collectionOperations/>
|
||||
<attribute name="validation_groups">sylius</attribute>
|
||||
|
||||
<collectionOperations>
|
||||
<collectionOperation name="admin_get">
|
||||
<attribute name="method">GET</attribute>
|
||||
<attribute name="filters">
|
||||
<attribute>sylius.api.promotion_coupon_order_filter</attribute>
|
||||
<attribute>Sylius\Bundle\ApiBundle\Filter\Doctrine\PromotionCouponPromotionFilter</attribute>
|
||||
</attribute>
|
||||
<attribute name="normalization_context">
|
||||
<attribute name="groups">admin:promotion_coupon:read</attribute>
|
||||
</attribute>
|
||||
<attribute name="order">
|
||||
<attribute name="used">DESC</attribute>
|
||||
</attribute>
|
||||
</collectionOperation>
|
||||
|
||||
<collectionOperation name="admin_post">
|
||||
<attribute name="method">POST</attribute>
|
||||
<attribute name="normalization_context">
|
||||
<attribute name="groups">admin:promotion_coupon:read</attribute>
|
||||
</attribute>
|
||||
<attribute name="denormalization_context">
|
||||
<attribute name="groups">admin:promotion_coupon:create</attribute>
|
||||
</attribute>
|
||||
</collectionOperation>
|
||||
</collectionOperations>
|
||||
|
||||
<itemOperations>
|
||||
<itemOperation name="admin_get">
|
||||
|
|
@ -27,8 +53,39 @@
|
|||
<attribute name="groups">admin:promotion_coupon:read</attribute>
|
||||
</attribute>
|
||||
</itemOperation>
|
||||
|
||||
<itemOperation name="admin_put">
|
||||
<attribute name="method">PUT</attribute>
|
||||
<attribute name="normalization_context">
|
||||
<attribute name="groups">admin:promotion_coupon:read</attribute>
|
||||
</attribute>
|
||||
<attribute name="denormalization_context">
|
||||
<attribute name="groups">admin:promotion_coupon:update</attribute>
|
||||
</attribute>
|
||||
</itemOperation>
|
||||
|
||||
<itemOperation name="admin_delete">
|
||||
<attribute name="method">DELETE</attribute>
|
||||
</itemOperation>
|
||||
</itemOperations>
|
||||
|
||||
<property name="id" identifier="true" writable="false" />
|
||||
<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" />
|
||||
<property name="perCustomerUsageLimit" readable="true" />
|
||||
<property name="used" readable="true" />
|
||||
<property name="reusableFromCancelledOrders" readable="true" />
|
||||
<property name="promotion" readable="true" />
|
||||
<property name="expiresAt" readable="true" />
|
||||
<property name="createdAt" readable="true" />
|
||||
<property name="updatedAt" readable="true" />
|
||||
</resource>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" ?>
|
||||
|
||||
<!--
|
||||
|
||||
This file is part of the Sylius package.
|
||||
|
||||
(c) Sylius Sp. z o.o.
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
|
||||
-->
|
||||
|
||||
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
|
||||
>
|
||||
<class name="Sylius\Component\Core\Model\PromotionCoupon">
|
||||
<attribute name="createdAt">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="updatedAt">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="code">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
<group>admin:promotion_coupon:create</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="usageLimit">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
<group>admin:promotion_coupon:create</group>
|
||||
<group>admin:promotion_coupon:update</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="perCustomerUsageLimit">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
<group>admin:promotion_coupon:create</group>
|
||||
<group>admin:promotion_coupon:update</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="used">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="reusableFromCancelledOrders">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
<group>admin:promotion_coupon:create</group>
|
||||
<group>admin:promotion_coupon:update</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="promotion">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
<group>admin:promotion_coupon:create</group>
|
||||
</attribute>
|
||||
|
||||
<attribute name="expiresAt">
|
||||
<group>admin:promotion_coupon:read</group>
|
||||
<group>admin:promotion_coupon:create</group>
|
||||
<group>admin:promotion_coupon:update</group>
|
||||
</attribute>
|
||||
</class>
|
||||
</serializer>
|
||||
|
|
@ -85,6 +85,11 @@
|
|||
<tag name="api_platform.data_persister" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataPersister\PromotionCouponDataPersister">
|
||||
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
|
||||
<tag name="api_platform.data_persister" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\DataPersister\TranslatableDataPersister">
|
||||
<argument type="service" id="sylius.translation_locale_provider" />
|
||||
<tag name="api_platform.data_persister" priority="128" />
|
||||
|
|
|
|||
|
|
@ -315,5 +315,22 @@
|
|||
</argument>
|
||||
<tag name="api_platform.filter" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.api.promotion_coupon_order_filter" parent="api_platform.doctrine.orm.order_filter" public="true">
|
||||
<argument type="collection">
|
||||
<argument key="code" />
|
||||
<argument key="expiresAt" />
|
||||
<argument key="usageLimit" />
|
||||
<argument key="perCustomerUsageLimit" />
|
||||
<argument key="used" />
|
||||
</argument>
|
||||
<tag name="api_platform.filter" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\ApiBundle\Filter\Doctrine\PromotionCouponPromotionFilter" public="true">
|
||||
<argument type="service" id="api_platform.symfony.iri_converter" />
|
||||
<argument type="service" id="doctrine" />
|
||||
<tag name="api_platform.filter" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace spec\Sylius\Bundle\ApiBundle\DataPersister;
|
||||
|
||||
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
|
||||
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\ApiBundle\Exception\PromotionCouponCannotBeRemoved;
|
||||
use Sylius\Component\Core\Model\PromotionCouponInterface;
|
||||
|
||||
final class PromotionCouponDataPersisterSpec extends ObjectBehavior
|
||||
{
|
||||
function let(ContextAwareDataPersisterInterface $dataPersister): void
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,5 +50,9 @@
|
|||
<argument>%sylius.promotion_rules%</argument>
|
||||
<tag name="validator.constraint_validator" alias="sylius_promotion_rule" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\PromotionBundle\Validator\PromotionNotCouponBasedValidator">
|
||||
<tag name="validator.constraint_validator" alias="sylius_promotion_not_coupon_based_validator" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@
|
|||
<option name="message">sylius.promotion_coupon.code.unique</option>
|
||||
<option name="groups">sylius</option>
|
||||
</constraint>
|
||||
<constraint name="Sylius\Bundle\PromotionBundle\Validator\Constraints\PromotionNotCouponBased">
|
||||
<option name="groups">sylius</option>
|
||||
</constraint>
|
||||
|
||||
<property name="promotion">
|
||||
<constraint name="NotBlank">
|
||||
<option name="message">sylius.promotion_coupon.promotion.not_blank</option>
|
||||
<option name="groups">sylius</option>
|
||||
</constraint>
|
||||
</property>
|
||||
|
||||
<property name="code">
|
||||
<constraint name="NotBlank">
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\PromotionBundle\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
final class PromotionNotCouponBased extends Constraint
|
||||
{
|
||||
public string $message = 'sylius.promotion_coupon.promotion.not_coupon_based';
|
||||
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
public function validatedBy(): string
|
||||
{
|
||||
return 'sylius_promotion_not_coupon_based_validator';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\PromotionBundle\Validator;
|
||||
|
||||
use Sylius\Bundle\PromotionBundle\Validator\Constraints\PromotionNotCouponBased;
|
||||
use Sylius\Component\Promotion\Model\PromotionCouponInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
|
||||
final class PromotionNotCouponBasedValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (null === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$constraint instanceof PromotionNotCouponBased) {
|
||||
throw new UnexpectedTypeException($constraint, PromotionNotCouponBased::class);
|
||||
}
|
||||
|
||||
if (!$value instanceof PromotionCouponInterface) {
|
||||
throw new UnexpectedValueException($value, PromotionCouponInterface::class);
|
||||
}
|
||||
|
||||
$promotion = $value->getPromotion();
|
||||
if (null === $promotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$promotion->isCouponBased()) {
|
||||
$this->context
|
||||
->buildViolation($constraint->message)
|
||||
->atPath('promotion')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace spec\Sylius\Bundle\PromotionBundle\Validator;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
use Sylius\Bundle\PromotionBundle\Validator\Constraints\PromotionNotCouponBased;
|
||||
use Sylius\Component\Promotion\Model\PromotionCouponInterface;
|
||||
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
|
||||
{
|
||||
function let(ExecutionContextInterface $context): void
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
161
tests/Api/Admin/PromotionCouponsTest.php
Normal file
161
tests/Api/Admin/PromotionCouponsTest.php
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Tests\Api\Admin;
|
||||
|
||||
use Sylius\Component\Core\Model\PromotionCouponInterface;
|
||||
use Sylius\Component\Core\Model\PromotionInterface;
|
||||
use Sylius\Tests\Api\JsonApiTestCase;
|
||||
use Sylius\Tests\Api\Utils\AdminUserLoginTrait;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class PromotionCouponsTest extends JsonApiTestCase
|
||||
{
|
||||
use AdminUserLoginTrait;
|
||||
|
||||
/** @test */
|
||||
public function it_gets_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: '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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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: <dateTimeBetween('now', '+20 days')>
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ Sylius\Component\Core\Model\Order:
|
|||
state: "new"
|
||||
tokenValue: "token"
|
||||
promotions: ['@promotion_50_off']
|
||||
promotionCoupon: '@promotion_1_off_coupon_1'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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@
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue