mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 17:10:53 +00:00
feature #15854 Archiving Cart Promotion (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 | #13475 | License | MIT Based on #15375, I have split this PR into "Archiving Cart Promotion" and "Archiving Catalog Promotion" There are two cases to consider: - Priority: it cannot be null and is assigned a "gedmo:sortable-position". This means that when we archive cart promotion, it still influences the position of all promotions. - API changes: I have added added PATCH operations, which removes all null values from responses in promotions. So we need to decide whether to add `skip_null_values: false` across all operations or not? Commits ------- [Promotion] Add archivedAt to Promotion [Promotion] Add promotion archiving [Promotion][Behat] Add promotion archiving tests [Promotion] Add PromotionArchivalEligibilityChecker class [Api][Promotion] Add archivedAt to promotion Fix phpstan [Behat][Ui] Add a method to check if a promotion exists without opening the index page [Behat][Ui] Add missing method [Behat] Move archived promotion test to receiving_discount folder
This commit is contained in:
commit
8e6b5915de
46 changed files with 825 additions and 5 deletions
|
|
@ -73,3 +73,14 @@ Feature: Order promotions integrity
|
||||||
And I proceeded with "Free" shipping method and "Offline" payment method
|
And I proceeded with "Free" shipping method and "Offline" payment method
|
||||||
When I confirm my order
|
When I confirm my order
|
||||||
Then I should see the thank you page
|
Then I should see the thank you page
|
||||||
|
|
||||||
|
@ui @api
|
||||||
|
Scenario: Preventing customer from completing checkout with already archived promotion
|
||||||
|
Given this promotion gives "$10.00" discount to every order
|
||||||
|
And I added product "PHP T-Shirt" to the cart
|
||||||
|
And I have specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow"
|
||||||
|
And I proceeded with "Free" shipping method and "Offline" payment method
|
||||||
|
And the promotion "Christmas sale" is archived
|
||||||
|
When I try to confirm my order
|
||||||
|
Then I should be informed that this promotion is no longer applied
|
||||||
|
And I should not see the thank you page
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
@managing_promotions
|
||||||
|
Feature: Archiving promotions
|
||||||
|
In order to hide no longer available promotions from the list and customers' use
|
||||||
|
As an Administrator
|
||||||
|
I want to archive promotions
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the store operates on a single channel in "United States"
|
||||||
|
And there is a promotion "Christmas sale"
|
||||||
|
And there is also a promotion "New Year sale"
|
||||||
|
And I am logged in as an administrator
|
||||||
|
|
||||||
|
@api @ui
|
||||||
|
Scenario: Archiving a promotion
|
||||||
|
When I browse promotions
|
||||||
|
And I archive the "Christmas sale" promotion
|
||||||
|
Then I should see a single promotion in the list
|
||||||
|
And I should see the promotion "New Year sale" in the list
|
||||||
|
|
||||||
|
@domain
|
||||||
|
Scenario: Archiving a promotion does not remove it from the database
|
||||||
|
When I archive the "Christmas sale" promotion
|
||||||
|
Then the promotion "Christmas sale" should still exist in the registry
|
||||||
|
|
||||||
|
@api @ui
|
||||||
|
Scenario: Seeing only archived promotions
|
||||||
|
Given the promotion "Christmas sale" is archived
|
||||||
|
When I browse promotions
|
||||||
|
And I filter archival promotions
|
||||||
|
Then I should see a single promotion in the list
|
||||||
|
And I should see the promotion "Christmas sale" in the list
|
||||||
|
And I should not see the promotion "New Year sale" in the list
|
||||||
|
|
||||||
|
@api @ui
|
||||||
|
Scenario: Restoring an archival promotion
|
||||||
|
Given the promotion "Christmas sale" is archived
|
||||||
|
When I browse promotions
|
||||||
|
And I filter archival promotions
|
||||||
|
And I restore the "Christmas sale" promotion
|
||||||
|
Then I should be viewing non archival promotions
|
||||||
|
And I should see 2 promotions on the list
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
@receiving_discount
|
||||||
|
Feature: Not applying archived promotions to the cart
|
||||||
|
In order to not apply promotions that are archived
|
||||||
|
As a Visitor
|
||||||
|
I want to not receive discounts from archived promotions
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the store operates on a single channel in "United States"
|
||||||
|
And the store has a product "PHP T-Shirt" priced at "$100.00"
|
||||||
|
And there is a promotion "Christmas sale"
|
||||||
|
And this promotion gives "$10.00" discount to every order
|
||||||
|
|
||||||
|
@api @ui
|
||||||
|
Scenario: Archived promotion is not applied to the cart
|
||||||
|
Given the promotion "Christmas sale" is archived
|
||||||
|
When I add product "PHP T-Shirt" to the cart
|
||||||
|
Then I should be on my cart summary page
|
||||||
|
And I should be notified that the product has been successfully added
|
||||||
|
And my cart total should be "$100.00"
|
||||||
|
|
@ -34,6 +34,7 @@ use Sylius\Component\Core\Promotion\Checker\Rule\HasTaxonRuleChecker;
|
||||||
use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker;
|
use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker;
|
||||||
use Sylius\Component\Customer\Model\CustomerGroupInterface;
|
use Sylius\Component\Customer\Model\CustomerGroupInterface;
|
||||||
use Sylius\Component\Promotion\Checker\Rule\ItemTotalRuleChecker;
|
use Sylius\Component\Promotion\Checker\Rule\ItemTotalRuleChecker;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Webmozart\Assert\Assert;
|
use Webmozart\Assert\Assert;
|
||||||
|
|
||||||
final class ManagingPromotionsContext implements Context
|
final class ManagingPromotionsContext implements Context
|
||||||
|
|
@ -73,6 +74,23 @@ final class ManagingPromotionsContext implements Context
|
||||||
$this->client->buildUpdateRequest(Resources::PROMOTIONS, $promotion->getCode());
|
$this->client->buildUpdateRequest(Resources::PROMOTIONS, $promotion->getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I archive the :promotion promotion
|
||||||
|
*/
|
||||||
|
public function iArchiveThePromotion(PromotionInterface $promotion): void
|
||||||
|
{
|
||||||
|
$this->client->customItemAction(Resources::PROMOTIONS, $promotion->getCode(), Request::METHOD_PATCH, 'archive');
|
||||||
|
$this->client->index(Resources::PROMOTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I restore the :promotion promotion
|
||||||
|
*/
|
||||||
|
public function iRestoreThePromotion(PromotionInterface $promotion): void
|
||||||
|
{
|
||||||
|
$this->client->customItemAction(Resources::PROMOTIONS, $promotion->getCode(), Request::METHOD_PATCH, 'restore');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @When I specify its :field as :value
|
* @When I specify its :field as :value
|
||||||
* @When I do not specify its :field
|
* @When I do not specify its :field
|
||||||
|
|
@ -410,6 +428,15 @@ final class ManagingPromotionsContext implements Context
|
||||||
$this->client->filter();
|
$this->client->filter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I filter archival promotions
|
||||||
|
*/
|
||||||
|
public function iFilterArchivalPromotions(): void
|
||||||
|
{
|
||||||
|
$this->client->addFilter('exists[archivedAt]', true);
|
||||||
|
$this->client->filter();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @When I add it
|
* @When I add it
|
||||||
* @When I try to add it
|
* @When I try to add it
|
||||||
|
|
@ -456,6 +483,17 @@ final class ManagingPromotionsContext implements Context
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I should not see the promotion :promotionName in the list
|
||||||
|
*/
|
||||||
|
public function iShouldNotSeeThePromotionInTheList(string $promotionName): void
|
||||||
|
{
|
||||||
|
Assert::false(
|
||||||
|
$this->responseChecker->hasItemWithValue($this->client->getLastResponse(), 'name', $promotionName),
|
||||||
|
sprintf('Promotion with name %s does not exist', $promotionName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then /^(this promotion) should be coupon based$/
|
* @Then /^(this promotion) should be coupon based$/
|
||||||
*/
|
*/
|
||||||
|
|
@ -847,6 +885,14 @@ final class ManagingPromotionsContext implements Context
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I should be viewing non archival promotions
|
||||||
|
*/
|
||||||
|
public function iShouldBeViewingNonArchivalPromotions(): void
|
||||||
|
{
|
||||||
|
$this->client->index(Resources::PROMOTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
private function addToRequestAction(string $type, array $configuration): void
|
private function addToRequestAction(string $type, array $configuration): void
|
||||||
{
|
{
|
||||||
$data['actions'][] = [
|
$data['actions'][] = [
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Domain;
|
||||||
|
|
||||||
use Behat\Behat\Context\Context;
|
use Behat\Behat\Context\Context;
|
||||||
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
|
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
use Sylius\Behat\Service\SharedStorageInterface;
|
use Sylius\Behat\Service\SharedStorageInterface;
|
||||||
use Sylius\Component\Promotion\Model\PromotionInterface;
|
use Sylius\Component\Promotion\Model\PromotionInterface;
|
||||||
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
|
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
|
||||||
|
|
@ -25,6 +26,7 @@ final class ManagingPromotionsContext implements Context
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private SharedStorageInterface $sharedStorage,
|
private SharedStorageInterface $sharedStorage,
|
||||||
private PromotionRepositoryInterface $promotionRepository,
|
private PromotionRepositoryInterface $promotionRepository,
|
||||||
|
private ObjectManager $promotionManager,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,6 +50,16 @@ final class ManagingPromotionsContext implements Context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I archive the :promotion promotion
|
||||||
|
*/
|
||||||
|
public function iArchiveThePromotion(PromotionInterface $promotion): void
|
||||||
|
{
|
||||||
|
$promotion->setArchivedAt(new \DateTime());
|
||||||
|
|
||||||
|
$this->promotionManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then /^(this promotion) should no longer exist in the promotion registry$/
|
* @Then /^(this promotion) should no longer exist in the promotion registry$/
|
||||||
*/
|
*/
|
||||||
|
|
@ -71,4 +83,12 @@ final class ManagingPromotionsContext implements Context
|
||||||
{
|
{
|
||||||
Assert::isInstanceOf($this->sharedStorage->get('last_exception'), ForeignKeyConstraintViolationException::class);
|
Assert::isInstanceOf($this->sharedStorage->get('last_exception'), ForeignKeyConstraintViolationException::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then the promotion :promotion should still exist in the registry
|
||||||
|
*/
|
||||||
|
public function thePromotionShouldStillExistInTheRegistry(PromotionInterface $promotion): void
|
||||||
|
{
|
||||||
|
Assert::notNull($this->promotionRepository->find($promotion));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,16 @@ final class PromotionContext implements Context
|
||||||
$this->objectManager->flush();
|
$this->objectManager->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Given the promotion :promotion is archived
|
||||||
|
*/
|
||||||
|
public function thisPromotionIsArchived(PromotionInterface $promotion): void
|
||||||
|
{
|
||||||
|
$promotion->setArchivedAt(new \DateTime());
|
||||||
|
|
||||||
|
$this->objectManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given /^(this coupon) has already expired$/
|
* @Given /^(this coupon) has already expired$/
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ final class ManagingPromotionsContext implements Context
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then I should see the promotion :promotionName in the list
|
|
||||||
* @Then the :promotionName promotion should appear in the registry
|
* @Then the :promotionName promotion should appear in the registry
|
||||||
* @Then the :promotionName promotion should exist in the registry
|
* @Then the :promotionName promotion should exist in the registry
|
||||||
* @Then this promotion should still be named :promotionName
|
* @Then this promotion should still be named :promotionName
|
||||||
|
|
@ -278,6 +277,33 @@ final class ManagingPromotionsContext implements Context
|
||||||
$this->indexPage->bulkDelete();
|
$this->indexPage->bulkDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I archive the :promotionName promotion
|
||||||
|
*/
|
||||||
|
public function iArchiveThePromotion(string $promotionName): void
|
||||||
|
{
|
||||||
|
$actions = $this->indexPage->getActionsForResource(['name' => $promotionName]);
|
||||||
|
$actions->pressButton('Archive');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I restore the :promotionName promotion
|
||||||
|
*/
|
||||||
|
public function iRestoreThePromotion(string $promotionName): void
|
||||||
|
{
|
||||||
|
$actions = $this->indexPage->getActionsForResource(['name' => $promotionName]);
|
||||||
|
$actions->pressButton('Restore');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I filter archival promotions
|
||||||
|
*/
|
||||||
|
public function iFilterArchivalPromotions(): void
|
||||||
|
{
|
||||||
|
$this->indexPage->chooseArchival('Yes');
|
||||||
|
$this->indexPage->filter();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then I should see a single promotion in the list
|
* @Then I should see a single promotion in the list
|
||||||
* @Then there should be :amount promotions
|
* @Then there should be :amount promotions
|
||||||
|
|
@ -829,6 +855,30 @@ final class ManagingPromotionsContext implements Context
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I should see the promotion :promotionName in the list
|
||||||
|
*/
|
||||||
|
public function iShouldSeeThePromotionInTheList(string $promotionName): void
|
||||||
|
{
|
||||||
|
Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $promotionName]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I should not see the promotion :promotionName in the list
|
||||||
|
*/
|
||||||
|
public function iShouldNotSeeThePromotionInTheList(string $promotionName): void
|
||||||
|
{
|
||||||
|
Assert::false($this->indexPage->isSingleResourceOnPage(['name' => $promotionName]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I should be viewing non archival promotions
|
||||||
|
*/
|
||||||
|
public function iShouldBeViewingNonArchivalPromotions(): void
|
||||||
|
{
|
||||||
|
Assert::false($this->indexPage->isArchivalFilterEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
private function assertFieldValidationMessage(string $element, string $expectedMessage)
|
private function assertFieldValidationMessage(string $element, string $expectedMessage)
|
||||||
{
|
{
|
||||||
/** @var CreatePageInterface|UpdatePageInterface $currentPage */
|
/** @var CreatePageInterface|UpdatePageInterface $currentPage */
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,23 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface
|
||||||
$this->getDocument()->fillField(sprintf('criteria_%s_value', $field), $value);
|
$this->getDocument()->fillField(sprintf('criteria_%s_value', $field), $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function chooseArchival(string $isArchival): void
|
||||||
|
{
|
||||||
|
$this->getElement('filter_archival')->selectOption($isArchival);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isArchivalFilterEnabled(): bool
|
||||||
|
{
|
||||||
|
return '1' === $this->getElement('filter_archival')->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefinedElements(): array
|
||||||
|
{
|
||||||
|
return array_merge(parent::getDefinedElements(), [
|
||||||
|
'filter_archival' => '#criteria_archival',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function getPromotionFieldsWithHeader(PromotionInterface $promotion, string $header): NodeElement
|
private function getPromotionFieldsWithHeader(PromotionInterface $promotion, string $header): NodeElement
|
||||||
{
|
{
|
||||||
$tableAccessor = $this->getTableAccessor();
|
$tableAccessor = $this->getTableAccessor();
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,8 @@ interface IndexPageInterface extends BaseIndexPageInterface
|
||||||
public function isAbleToManageCouponsFor(PromotionInterface $promotion): bool;
|
public function isAbleToManageCouponsFor(PromotionInterface $promotion): bool;
|
||||||
|
|
||||||
public function isCouponBasedFor(PromotionInterface $promotion): bool;
|
public function isCouponBasedFor(PromotionInterface $promotion): bool;
|
||||||
|
|
||||||
|
public function chooseArchival(string $isArchival): void;
|
||||||
|
|
||||||
|
public function isArchivalFilterEnabled(): bool;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
<service id="sylius.behat.context.domain.managing_promotions" class="Sylius\Behat\Context\Domain\ManagingPromotionsContext">
|
<service id="sylius.behat.context.domain.managing_promotions" class="Sylius\Behat\Context\Domain\ManagingPromotionsContext">
|
||||||
<argument type="service" id="sylius.behat.shared_storage" />
|
<argument type="service" id="sylius.behat.shared_storage" />
|
||||||
<argument type="service" id="sylius.repository.promotion" />
|
<argument type="service" id="sylius.repository.promotion" />
|
||||||
|
<argument type="service" id="sylius.manager.promotion" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service id="sylius.behat.context.domain.managing_promotion_coupons" class="Sylius\Behat\Context\Domain\ManagingPromotionCouponsContext">
|
<service id="sylius.behat.context.domain.managing_promotion_coupons" class="Sylius\Behat\Context\Domain\ManagingPromotionCouponsContext">
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ default:
|
||||||
- sylius.behat.context.transform.product
|
- sylius.behat.context.transform.product
|
||||||
- sylius.behat.context.transform.product_option
|
- sylius.behat.context.transform.product_option
|
||||||
- sylius.behat.context.transform.product_variant
|
- sylius.behat.context.transform.product_variant
|
||||||
|
- sylius.behat.context.transform.promotion
|
||||||
- sylius.behat.context.transform.province
|
- sylius.behat.context.transform.province
|
||||||
- sylius.behat.context.transform.shared_storage
|
- sylius.behat.context.transform.shared_storage
|
||||||
- sylius.behat.context.transform.shipping_category
|
- sylius.behat.context.transform.shipping_category
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ default:
|
||||||
- sylius.behat.context.transform.payment
|
- sylius.behat.context.transform.payment
|
||||||
- sylius.behat.context.transform.product
|
- sylius.behat.context.transform.product
|
||||||
- sylius.behat.context.transform.product_variant
|
- sylius.behat.context.transform.product_variant
|
||||||
|
- sylius.behat.context.transform.promotion
|
||||||
- sylius.behat.context.transform.shared_storage
|
- sylius.behat.context.transform.shared_storage
|
||||||
- sylius.behat.context.transform.taxon
|
- sylius.behat.context.transform.taxon
|
||||||
- sylius.behat.context.transform.shipping_method
|
- sylius.behat.context.transform.shipping_method
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ default:
|
||||||
- sylius.behat.context.transform.product
|
- sylius.behat.context.transform.product
|
||||||
- sylius.behat.context.transform.product_option
|
- sylius.behat.context.transform.product_option
|
||||||
- sylius.behat.context.transform.product_variant
|
- sylius.behat.context.transform.product_variant
|
||||||
|
- sylius.behat.context.transform.promotion
|
||||||
- sylius.behat.context.transform.province
|
- sylius.behat.context.transform.province
|
||||||
- sylius.behat.context.transform.shared_storage
|
- sylius.behat.context.transform.shared_storage
|
||||||
- sylius.behat.context.transform.shipping_category
|
- sylius.behat.context.transform.shipping_category
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ default:
|
||||||
- sylius.behat.context.transform.payment
|
- sylius.behat.context.transform.payment
|
||||||
- sylius.behat.context.transform.product
|
- sylius.behat.context.transform.product
|
||||||
- sylius.behat.context.transform.product_variant
|
- sylius.behat.context.transform.product_variant
|
||||||
|
- sylius.behat.context.transform.promotion
|
||||||
- sylius.behat.context.transform.shared_storage
|
- sylius.behat.context.transform.shared_storage
|
||||||
- sylius.behat.context.transform.taxon
|
- sylius.behat.context.transform.taxon
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ sylius_grid:
|
||||||
label: sylius.ui.coupon
|
label: sylius.ui.coupon
|
||||||
options:
|
options:
|
||||||
fields: [coupons.code]
|
fields: [coupons.code]
|
||||||
|
archival:
|
||||||
|
type: exists
|
||||||
|
label: sylius.ui.archival
|
||||||
|
options:
|
||||||
|
field: archivedAt
|
||||||
|
default_value: false
|
||||||
actions:
|
actions:
|
||||||
main:
|
main:
|
||||||
create:
|
create:
|
||||||
|
|
@ -85,6 +91,8 @@ sylius_grid:
|
||||||
type: update
|
type: update
|
||||||
delete:
|
delete:
|
||||||
type: delete
|
type: delete
|
||||||
|
archive:
|
||||||
|
type: archive
|
||||||
bulk:
|
bulk:
|
||||||
delete:
|
delete:
|
||||||
type: delete
|
type: delete
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,18 @@ sylius_admin_promotion:
|
||||||
form: "@SyliusAdmin/Promotion/_form.html.twig"
|
form: "@SyliusAdmin/Promotion/_form.html.twig"
|
||||||
toolbar: "@SyliusAdmin/Promotion/_toolbar.html.twig"
|
toolbar: "@SyliusAdmin/Promotion/_toolbar.html.twig"
|
||||||
type: sylius.resource
|
type: sylius.resource
|
||||||
|
|
||||||
|
sylius_admin_promotion_archive:
|
||||||
|
path: /promotions/{id}/archive
|
||||||
|
methods: [PATCH]
|
||||||
|
defaults:
|
||||||
|
_controller: sylius.controller.promotion::updateAction
|
||||||
|
_sylius:
|
||||||
|
section: admin
|
||||||
|
permission: true
|
||||||
|
template: '@SyliusUi/Grid/Action/archive.html.twig'
|
||||||
|
form:
|
||||||
|
type: Sylius\Bundle\ResourceBundle\Form\Type\ArchivableType
|
||||||
|
redirect:
|
||||||
|
route: sylius_admin_promotion_index
|
||||||
|
parameters: {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?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\Applicator;
|
||||||
|
|
||||||
|
use Sylius\Calendar\Provider\DateTimeProviderInterface;
|
||||||
|
use Sylius\Component\Core\Model\PromotionInterface;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class ArchivingPromotionApplicator implements ArchivingPromotionApplicatorInterface
|
||||||
|
{
|
||||||
|
public function __construct(private DateTimeProviderInterface $calendar)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function archive(PromotionInterface $data): PromotionInterface
|
||||||
|
{
|
||||||
|
$data->setArchivedAt($this->calendar->now());
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restore(PromotionInterface $data): PromotionInterface
|
||||||
|
{
|
||||||
|
$data->setArchivedAt(null);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?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\Applicator;
|
||||||
|
|
||||||
|
use Sylius\Component\Core\Model\PromotionInterface;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
interface ArchivingPromotionApplicatorInterface
|
||||||
|
{
|
||||||
|
public function archive(PromotionInterface $data): PromotionInterface;
|
||||||
|
|
||||||
|
public function restore(PromotionInterface $data): PromotionInterface;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?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\Doctrine\QueryCollectionExtension;
|
||||||
|
|
||||||
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface;
|
||||||
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class HideArchivedPromotionExtension implements ContextAwareQueryCollectionExtensionInterface
|
||||||
|
{
|
||||||
|
public function __construct(private string $promotionClass)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<array-key, mixed> $context */
|
||||||
|
public function applyToCollection(
|
||||||
|
QueryBuilder $queryBuilder,
|
||||||
|
QueryNameGeneratorInterface $queryNameGenerator,
|
||||||
|
string $resourceClass,
|
||||||
|
string $operationName = null,
|
||||||
|
array $context = [],
|
||||||
|
): void {
|
||||||
|
if ($this->promotionClass !== $resourceClass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($context['filters']['exists']['archivedAt'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootAlias = $queryBuilder->getRootAliases()[0];
|
||||||
|
|
||||||
|
$queryBuilder->andWhere($queryBuilder->expr()->isNull(sprintf('%s.archivedAt', $rootAlias)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,8 @@
|
||||||
<attribute>admin:promotion:show</attribute>
|
<attribute>admin:promotion:show</attribute>
|
||||||
<attribute>sylius:admin:promotion:show</attribute>
|
<attribute>sylius:admin:promotion:show</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<!-- It auto-turned on when adding PATCH itemOperations: https://github.com/api-platform/core/issues/3600 -->
|
||||||
|
<attribute name="skip_null_values">false</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="openapi_context">
|
<attribute name="openapi_context">
|
||||||
<attribute name="description">
|
<attribute name="description">
|
||||||
|
|
@ -81,10 +83,13 @@ Example configuration for `order_fixed_discount` action type:
|
||||||
<attribute>admin:promotion:index</attribute>
|
<attribute>admin:promotion:index</attribute>
|
||||||
<attribute>sylius:admin:promotion:index</attribute>
|
<attribute>sylius:admin:promotion:index</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<!-- It auto-turned on when adding PATCH itemOperations: https://github.com/api-platform/core/issues/3600 -->
|
||||||
|
<attribute name="skip_null_values">false</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="filters">
|
<attribute name="filters">
|
||||||
<attribute>sylius.api.promotion_coupon_search_filter</attribute>
|
<attribute>sylius.api.promotion_coupon_search_filter</attribute>
|
||||||
<attribute>sylius.api.promotion_order_filter</attribute>
|
<attribute>sylius.api.promotion_order_filter</attribute>
|
||||||
|
<attribute>sylius.api.exists_filter.archived_at</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
</collectionOperation>
|
</collectionOperation>
|
||||||
</collectionOperations>
|
</collectionOperations>
|
||||||
|
|
@ -97,6 +102,8 @@ Example configuration for `order_fixed_discount` action type:
|
||||||
<attribute>admin:promotion:show</attribute>
|
<attribute>admin:promotion:show</attribute>
|
||||||
<attribute>sylius:admin:promotion:show</attribute>
|
<attribute>sylius:admin:promotion:show</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<!-- It auto-turned on when adding PATCH itemOperations: https://github.com/api-platform/core/issues/3600 -->
|
||||||
|
<attribute name="skip_null_values">false</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
</itemOperation>
|
</itemOperation>
|
||||||
|
|
||||||
|
|
@ -113,6 +120,8 @@ Example configuration for `order_fixed_discount` action type:
|
||||||
<attribute>admin:promotion:show</attribute>
|
<attribute>admin:promotion:show</attribute>
|
||||||
<attribute>sylius:admin:promotion:show</attribute>
|
<attribute>sylius:admin:promotion:show</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<!-- It auto-turned on when adding PATCH itemOperations: https://github.com/api-platform/core/issues/3600 -->
|
||||||
|
<attribute name="skip_null_values">false</attribute>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="openapi_context">
|
<attribute name="openapi_context">
|
||||||
<attribute name="description">
|
<attribute name="description">
|
||||||
|
|
@ -146,6 +155,37 @@ Example configuration for `order_fixed_discount` action type:
|
||||||
</attribute>
|
</attribute>
|
||||||
</itemOperation>
|
</itemOperation>
|
||||||
|
|
||||||
|
<itemOperation name="admin_archive">
|
||||||
|
<attribute name="method">PATCH</attribute>
|
||||||
|
<attribute name="path">/promotions/{code}/archive</attribute>
|
||||||
|
<attribute name="input">false</attribute>
|
||||||
|
<attribute name="controller">Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface::archive</attribute>
|
||||||
|
<attribute name="openapi_context">
|
||||||
|
<attribute name="summary">Archives Promotion</attribute>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="normalization_context">
|
||||||
|
<attribute name="groups">
|
||||||
|
<attribute>admin:promotion:read</attribute>
|
||||||
|
<attribute>sylius:admin:promotion:read</attribute>
|
||||||
|
</attribute>
|
||||||
|
</attribute>
|
||||||
|
</itemOperation>
|
||||||
|
<itemOperation name="admin_restore">
|
||||||
|
<attribute name="method">PATCH</attribute>
|
||||||
|
<attribute name="path">/promotions/{code}/restore</attribute>
|
||||||
|
<attribute name="input">false</attribute>
|
||||||
|
<attribute name="controller">Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface::restore</attribute>
|
||||||
|
<attribute name="openapi_context">
|
||||||
|
<attribute name="summary">Restores Archived Promotion</attribute>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="normalization_context">
|
||||||
|
<attribute name="groups">
|
||||||
|
<attribute>admin:promotion:read</attribute>
|
||||||
|
<attribute>sylius:admin:promotion:read</attribute>
|
||||||
|
</attribute>
|
||||||
|
</attribute>
|
||||||
|
</itemOperation>
|
||||||
|
|
||||||
<itemOperation name="admin_delete">
|
<itemOperation name="admin_delete">
|
||||||
<attribute name="method">DELETE</attribute>
|
<attribute name="method">DELETE</attribute>
|
||||||
</itemOperation>
|
</itemOperation>
|
||||||
|
|
|
||||||
|
|
@ -198,5 +198,10 @@
|
||||||
<group>admin:promotion:update</group>
|
<group>admin:promotion:update</group>
|
||||||
<group>sylius:admin:promotion:update</group>
|
<group>sylius:admin:promotion:update</group>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
|
||||||
|
<attribute name="archivedAt">
|
||||||
|
<group>admin:promotion:read</group>
|
||||||
|
<group>sylius:admin:promotion:read</group>
|
||||||
|
</attribute>
|
||||||
</class>
|
</class>
|
||||||
</serializer>
|
</serializer>
|
||||||
|
|
|
||||||
|
|
@ -33,5 +33,10 @@
|
||||||
<service id="Sylius\Bundle\ApiBundle\Applicator\ProductReviewStateMachineTransitionApplicatorInterface" class="Sylius\Bundle\ApiBundle\Applicator\ProductReviewStateMachineTransitionApplicator">
|
<service id="Sylius\Bundle\ApiBundle\Applicator\ProductReviewStateMachineTransitionApplicatorInterface" class="Sylius\Bundle\ApiBundle\Applicator\ProductReviewStateMachineTransitionApplicator">
|
||||||
<argument id="sm.factory" type="service" />
|
<argument id="sm.factory" type="service" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="sylius.api.applicator.archiving_promotion" class="Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicator">
|
||||||
|
<argument type="service" id="Sylius\Calendar\Provider\DateTimeProviderInterface" />
|
||||||
|
</service>
|
||||||
|
<service id="Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface" alias="sylius.api.applicator.archiving_promotion" />
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
|
|
||||||
|
|
@ -140,5 +140,10 @@
|
||||||
<argument type="service" id="Sylius\Bundle\ApiBundle\Context\UserContextInterface" />
|
<argument type="service" id="Sylius\Bundle\ApiBundle\Context\UserContextInterface" />
|
||||||
<tag name="api_platform.doctrine.orm.query_extension.item" />
|
<tag name="api_platform.doctrine.orm.query_extension.item" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="Sylius\Bundle\ApiBundle\Doctrine\QueryCollectionExtension\HideArchivedPromotionExtension">
|
||||||
|
<argument>%sylius.model.promotion.class%</argument>
|
||||||
|
<tag name="api_platform.doctrine.orm.query_extension.collection" />
|
||||||
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
|
|
||||||
|
|
@ -403,5 +403,12 @@
|
||||||
</argument>
|
</argument>
|
||||||
<tag name="api_platform.filter" />
|
<tag name="api_platform.filter" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="sylius.api.exists_filter.archived_at" parent="api_platform.doctrine.orm.exists_filter" public="true">
|
||||||
|
<argument type="collection">
|
||||||
|
<argument key="archivedAt" />
|
||||||
|
</argument>
|
||||||
|
<tag name="api_platform.filter" />
|
||||||
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?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\Applicator;
|
||||||
|
|
||||||
|
use PhpSpec\ObjectBehavior;
|
||||||
|
use Sylius\Calendar\Provider\DateTimeProviderInterface;
|
||||||
|
use Sylius\Component\Core\Model\PromotionInterface;
|
||||||
|
|
||||||
|
final class ArchivingPromotionApplicatorSpec extends ObjectBehavior
|
||||||
|
{
|
||||||
|
function let(DateTimeProviderInterface $calendar)
|
||||||
|
{
|
||||||
|
$this->beConstructedWith($calendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_archives_promotion(
|
||||||
|
DateTimeProviderInterface $calendar,
|
||||||
|
PromotionInterface $promotion,
|
||||||
|
): void {
|
||||||
|
$now = new \DateTime();
|
||||||
|
$calendar->now()->willReturn($now);
|
||||||
|
|
||||||
|
$promotion->setArchivedAt($now)->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$this->archive($promotion)->shouldReturn($promotion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_restores_promotion(
|
||||||
|
PromotionInterface $promotion,
|
||||||
|
): void {
|
||||||
|
$promotion->setArchivedAt(null)->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$this->restore($promotion)->shouldReturn($promotion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?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\Doctrine\QueryCollectionExtension;
|
||||||
|
|
||||||
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||||
|
use Doctrine\ORM\Query\Expr;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use PhpSpec\ObjectBehavior;
|
||||||
|
|
||||||
|
final class HideArchivedPromotionExtensionSpec extends ObjectBehavior
|
||||||
|
{
|
||||||
|
function let()
|
||||||
|
{
|
||||||
|
$this->beConstructedWith('promotionClass');
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_does_nothing_if_current_resource_is_not_a_promotion(
|
||||||
|
QueryBuilder $queryBuilder,
|
||||||
|
QueryNameGeneratorInterface $queryNameGenerator,
|
||||||
|
): void {
|
||||||
|
$queryBuilder->getRootAliases()->shouldNotBeCalled();
|
||||||
|
$queryBuilder->andWhere()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$this->applyToCollection($queryBuilder, $queryNameGenerator, 'taxonClass', 'get', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_does_nothing_if_archived_at_filter_is_already_applied(
|
||||||
|
QueryBuilder $queryBuilder,
|
||||||
|
QueryNameGeneratorInterface $queryNameGenerator,
|
||||||
|
): void {
|
||||||
|
$queryBuilder->getRootAliases()->shouldNotBeCalled();
|
||||||
|
$queryBuilder->andWhere()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$this->applyToCollection($queryBuilder, $queryNameGenerator, 'promotionClass', 'get', ['filters' => ['exists' => ['archivedAt' => 'true']]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_filters_archived_promotions(
|
||||||
|
QueryBuilder $queryBuilder,
|
||||||
|
Expr $expr,
|
||||||
|
QueryNameGeneratorInterface $queryNameGenerator,
|
||||||
|
): void {
|
||||||
|
$queryBuilder->getRootAliases()->willReturn(['o']);
|
||||||
|
|
||||||
|
$expr->isNull('o.archivedAt')->willReturn('o.archivedAt IS NULL');
|
||||||
|
$queryBuilder->expr()->willReturn($expr);
|
||||||
|
$queryBuilder->andWhere('o.archivedAt IS NULL')->shouldBeCalled();
|
||||||
|
|
||||||
|
$this->applyToCollection($queryBuilder, $queryNameGenerator, 'promotionClass', 'get', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,10 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF
|
||||||
$promotion->setEndsAt(new \DateTime($options['ends_at']));
|
$promotion->setEndsAt(new \DateTime($options['ends_at']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($options['archived_at'])) {
|
||||||
|
$promotion->setArchivedAt(new \DateTime($options['archived_at']));
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($options['channels'] as $channel) {
|
foreach ($options['channels'] as $channel) {
|
||||||
$promotion->addChannel($channel);
|
$promotion->addChannel($channel);
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +143,8 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF
|
||||||
->setAllowedTypes('starts_at', ['null', 'string'])
|
->setAllowedTypes('starts_at', ['null', 'string'])
|
||||||
->setDefault('ends_at', null)
|
->setDefault('ends_at', null)
|
||||||
->setAllowedTypes('ends_at', ['null', 'string'])
|
->setAllowedTypes('ends_at', ['null', 'string'])
|
||||||
|
->setDefault('archived_at', null)
|
||||||
|
->setAllowedTypes('archived_at', ['null', 'string'])
|
||||||
->setDefault('channels', LazyOption::all($this->channelRepository))
|
->setDefault('channels', LazyOption::all($this->channelRepository))
|
||||||
->setAllowedTypes('channels', 'array')
|
->setAllowedTypes('channels', 'array')
|
||||||
->setNormalizer('channels', LazyOption::findBy($this->channelRepository, 'code'))
|
->setNormalizer('channels', LazyOption::findBy($this->channelRepository, 'code'))
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class PromotionFixture extends AbstractResourceFixture
|
||||||
->end()
|
->end()
|
||||||
->scalarNode('starts_at')->cannotBeEmpty()->end()
|
->scalarNode('starts_at')->cannotBeEmpty()->end()
|
||||||
->scalarNode('ends_at')->cannotBeEmpty()->end()
|
->scalarNode('ends_at')->cannotBeEmpty()->end()
|
||||||
|
->scalarNode('archived_at')->defaultNull()->end()
|
||||||
->arrayNode('rules')
|
->arrayNode('rules')
|
||||||
->requiresAtLeastOneElement()
|
->requiresAtLeastOneElement()
|
||||||
->arrayPrototype()
|
->arrayPrototype()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\CoreBundle\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20240216110238 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add archived_at field to promotion table.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE sylius_promotion ADD archived_at DATETIME DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE sylius_promotion DROP archived_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\CoreBundle\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractPostgreSQLMigration;
|
||||||
|
|
||||||
|
final class Version20240216110239 extends AbstractPostgreSQLMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add archived_at field to promotion table.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE sylius_promotion ADD archived_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE sylius_promotion DROP archived_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,23 @@ sylius_fixtures:
|
||||||
configuration:
|
configuration:
|
||||||
FASHION_WEB:
|
FASHION_WEB:
|
||||||
amount: 10.00
|
amount: 10.00
|
||||||
|
back_to_school:
|
||||||
|
code: 'back_to_school'
|
||||||
|
name: 'Back to school'
|
||||||
|
channels:
|
||||||
|
- 'FASHION_WEB'
|
||||||
|
starts_at: 'September 1 last year'
|
||||||
|
ends_at: 'September 30 last year'
|
||||||
|
archived_at: 'October 1 last year'
|
||||||
|
rules:
|
||||||
|
- type: 'item_total'
|
||||||
|
configuration:
|
||||||
|
FASHION_WEB:
|
||||||
|
amount: 100.00
|
||||||
|
actions:
|
||||||
|
- type: 'order_percentage_discount'
|
||||||
|
configuration:
|
||||||
|
percentage: 10
|
||||||
|
|
||||||
catalog_promotion:
|
catalog_promotion:
|
||||||
options:
|
options:
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
<option name="default">1</option>
|
<option name="default">1</option>
|
||||||
</options>
|
</options>
|
||||||
</field>
|
</field>
|
||||||
|
<field name="archivedAt" column="archived_at" type="datetime" nullable="true" />
|
||||||
|
|
||||||
<one-to-many field="coupons" target-entity="Sylius\Component\Promotion\Model\PromotionCouponInterface" mapped-by="promotion">
|
<one-to-many field="coupons" target-entity="Sylius\Component\Promotion\Model\PromotionCouponInterface" mapped-by="promotion">
|
||||||
<cascade>
|
<cascade>
|
||||||
|
|
|
||||||
|
|
@ -55,5 +55,10 @@
|
||||||
class="Sylius\Component\Promotion\Checker\Eligibility\CompositePromotionEligibilityChecker">
|
class="Sylius\Component\Promotion\Checker\Eligibility\CompositePromotionEligibilityChecker">
|
||||||
<argument type="collection" />
|
<argument type="collection" />
|
||||||
</service>
|
</service>
|
||||||
|
<service id="sylius.promotion_archival_eligibility_checker"
|
||||||
|
class="Sylius\Component\Promotion\Checker\Eligibility\PromotionArchivalEligibilityChecker"
|
||||||
|
public="false">
|
||||||
|
<tag name="sylius.promotion_eligibility_checker" />
|
||||||
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?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\Component\Promotion\Checker\Eligibility;
|
||||||
|
|
||||||
|
use Sylius\Component\Promotion\Model\PromotionInterface;
|
||||||
|
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
|
||||||
|
|
||||||
|
final class PromotionArchivalEligibilityChecker implements PromotionEligibilityCheckerInterface
|
||||||
|
{
|
||||||
|
public function isEligible(PromotionSubjectInterface $promotionSubject, PromotionInterface $promotion): bool
|
||||||
|
{
|
||||||
|
return null === $promotion->getArchivedAt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,13 +15,14 @@ namespace Sylius\Component\Promotion\Model;
|
||||||
|
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Sylius\Component\Resource\Model\ArchivableTrait;
|
||||||
use Sylius\Component\Resource\Model\TimestampableTrait;
|
use Sylius\Component\Resource\Model\TimestampableTrait;
|
||||||
use Sylius\Component\Resource\Model\TranslatableTrait;
|
use Sylius\Component\Resource\Model\TranslatableTrait;
|
||||||
use Sylius\Component\Resource\Model\TranslationInterface;
|
use Sylius\Component\Resource\Model\TranslationInterface;
|
||||||
|
|
||||||
class Promotion implements PromotionInterface
|
class Promotion implements PromotionInterface
|
||||||
{
|
{
|
||||||
use TimestampableTrait, TranslatableTrait {
|
use ArchivableTrait, TimestampableTrait, TranslatableTrait {
|
||||||
__construct as private initializeTranslationsCollection;
|
__construct as private initializeTranslationsCollection;
|
||||||
getTranslation as private doGetTranslation;
|
getTranslation as private doGetTranslation;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,13 @@ declare(strict_types=1);
|
||||||
namespace Sylius\Component\Promotion\Model;
|
namespace Sylius\Component\Promotion\Model;
|
||||||
|
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Sylius\Component\Resource\Model\ArchivableInterface;
|
||||||
use Sylius\Component\Resource\Model\CodeAwareInterface;
|
use Sylius\Component\Resource\Model\CodeAwareInterface;
|
||||||
use Sylius\Component\Resource\Model\ResourceInterface;
|
use Sylius\Component\Resource\Model\ResourceInterface;
|
||||||
use Sylius\Component\Resource\Model\TimestampableInterface;
|
use Sylius\Component\Resource\Model\TimestampableInterface;
|
||||||
use Sylius\Component\Resource\Model\TranslatableInterface;
|
use Sylius\Component\Resource\Model\TranslatableInterface;
|
||||||
|
|
||||||
interface PromotionInterface extends CodeAwareInterface, TimestampableInterface, TranslatableInterface, ResourceInterface
|
interface PromotionInterface extends ArchivableInterface, CodeAwareInterface, TimestampableInterface, TranslatableInterface, ResourceInterface
|
||||||
{
|
{
|
||||||
public function getName(): ?string;
|
public function getName(): ?string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?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\Component\Promotion\Checker\Eligibility;
|
||||||
|
|
||||||
|
use PhpSpec\ObjectBehavior;
|
||||||
|
use Sylius\Component\Promotion\Checker\Eligibility\PromotionEligibilityCheckerInterface;
|
||||||
|
use Sylius\Component\Promotion\Model\PromotionInterface;
|
||||||
|
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
|
||||||
|
|
||||||
|
final class PromotionArchivalEligibilityCheckerSpec extends ObjectBehavior
|
||||||
|
{
|
||||||
|
function it_is_a_promotion_eligibility_checker(): void
|
||||||
|
{
|
||||||
|
$this->shouldImplement(PromotionEligibilityCheckerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_is_eligible_when_archived_at_is_null(
|
||||||
|
PromotionSubjectInterface $promotionSubject,
|
||||||
|
PromotionInterface $promotion,
|
||||||
|
): void {
|
||||||
|
$promotion->getArchivedAt()->willReturn(null);
|
||||||
|
|
||||||
|
$this->isEligible($promotionSubject, $promotion)->shouldReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_is_not_eligible_when_archived_at_is_not_null(
|
||||||
|
PromotionSubjectInterface $promotionSubject,
|
||||||
|
PromotionInterface $promotion,
|
||||||
|
): void {
|
||||||
|
$promotion->getArchivedAt()->willReturn(new \DateTime());
|
||||||
|
|
||||||
|
$this->isEligible($promotionSubject, $promotion)->shouldReturn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -576,6 +576,56 @@ final class PromotionsTest extends JsonApiTestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_archives_a_promotion(): void
|
||||||
|
{
|
||||||
|
$fixtures = $this->loadFixturesFromFiles([
|
||||||
|
'authentication/api_administrator.yaml',
|
||||||
|
'channel.yaml',
|
||||||
|
'promotion/promotion.yaml'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var PromotionInterface $promotion */
|
||||||
|
$promotion = $fixtures['promotion_50_off'];
|
||||||
|
|
||||||
|
$this->client->request(
|
||||||
|
method: 'PATCH',
|
||||||
|
uri: sprintf('/api/v2/admin/promotions/%s/archive', $promotion->getCode()),
|
||||||
|
server: $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponse(
|
||||||
|
$this->client->getResponse(),
|
||||||
|
'admin/promotion/archive_promotion',
|
||||||
|
Response::HTTP_OK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_restores_a_promotion(): void
|
||||||
|
{
|
||||||
|
$fixtures = $this->loadFixturesFromFiles([
|
||||||
|
'authentication/api_administrator.yaml',
|
||||||
|
'channel.yaml',
|
||||||
|
'promotion/promotion.yaml'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var PromotionInterface $promotion */
|
||||||
|
$promotion = $fixtures['promotion_back_to_school'];
|
||||||
|
|
||||||
|
$this->client->request(
|
||||||
|
method: 'PATCH',
|
||||||
|
uri: sprintf('/api/v2/admin/promotions/%s/restore', $promotion->getCode()),
|
||||||
|
server: $this->headerBuilder()->withJsonLdAccept()->withAdminUserAuthorization('api@example.com')->build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponse(
|
||||||
|
$this->client->getResponse(),
|
||||||
|
'admin/promotion/restore_promotion',
|
||||||
|
Response::HTTP_OK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function it_deletes_a_promotion(): void
|
public function it_deletes_a_promotion(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,18 @@ Sylius\Component\Core\Model\Promotion:
|
||||||
couponBased: true
|
couponBased: true
|
||||||
translations:
|
translations:
|
||||||
- '@promotion_1_off_en'
|
- '@promotion_1_off_en'
|
||||||
|
promotion_back_to_school:
|
||||||
|
code: 'back_to_school'
|
||||||
|
name: 'Back to school'
|
||||||
|
description: 'Get 10% off on all school supplies'
|
||||||
|
channels: ['@channel_web', '@channel_mobile']
|
||||||
|
priority: 3
|
||||||
|
exclusive: false
|
||||||
|
appliesToDiscounted: true
|
||||||
|
couponBased: false
|
||||||
|
archivedAt: <dateTimeBetween('-1 month', 'now')>
|
||||||
|
translations:
|
||||||
|
- '@promotion_back_to_school_en'
|
||||||
|
|
||||||
Sylius\Component\Promotion\Model\PromotionTranslation:
|
Sylius\Component\Promotion\Model\PromotionTranslation:
|
||||||
promotion_50_off_en:
|
promotion_50_off_en:
|
||||||
|
|
@ -33,6 +45,10 @@ Sylius\Component\Promotion\Model\PromotionTranslation:
|
||||||
locale: 'en_US'
|
locale: 'en_US'
|
||||||
label: '1$ off every item!'
|
label: '1$ off every item!'
|
||||||
translatable: '@promotion_1_off'
|
translatable: '@promotion_1_off'
|
||||||
|
promotion_back_to_school_en:
|
||||||
|
locale: 'en_US'
|
||||||
|
label: 'Back to school sale!'
|
||||||
|
translatable: '@promotion_back_to_school'
|
||||||
|
|
||||||
Sylius\Component\Core\Model\PromotionCoupon:
|
Sylius\Component\Core\Model\PromotionCoupon:
|
||||||
promotion_1_off_coupon_1:
|
promotion_1_off_coupon_1:
|
||||||
|
|
|
||||||
32
tests/Api/Responses/admin/promotion/archive_promotion.json
Normal file
32
tests/Api/Responses/admin/promotion/archive_promotion.json
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"@context": "\/api\/v2\/contexts\/Promotion",
|
||||||
|
"@id": "\/api\/v2\/admin\/promotions\/50_off",
|
||||||
|
"@type": "Promotion",
|
||||||
|
"channels": [
|
||||||
|
"\/api\/v2\/admin\/channels\/MOBILE"
|
||||||
|
],
|
||||||
|
"id": @integer@,
|
||||||
|
"code": "50_off",
|
||||||
|
"name": "50% Off on your first order",
|
||||||
|
"description": "Get 50% off of your first purchase",
|
||||||
|
"priority": 1,
|
||||||
|
"exclusive": true,
|
||||||
|
"usageLimit": 1,
|
||||||
|
"used": 0,
|
||||||
|
"couponBased": false,
|
||||||
|
"coupons": [],
|
||||||
|
"rules": [],
|
||||||
|
"actions": [],
|
||||||
|
"appliesToDiscounted": false,
|
||||||
|
"archivedAt": @date@,
|
||||||
|
"createdAt": @date@,
|
||||||
|
"updatedAt": @date@,
|
||||||
|
"translations": {
|
||||||
|
"en_US": {
|
||||||
|
"@id": "\/api\/v2\/admin\/promotion-translations\/@integer@",
|
||||||
|
"@type": "PromotionTranslation",
|
||||||
|
"id": @integer@,
|
||||||
|
"label": "-50% on first order!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"coupons": [],
|
"coupons": [],
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
"archivedAt": null,
|
||||||
"createdAt": @date@,
|
"createdAt": @date@,
|
||||||
"updatedAt": @date@,
|
"updatedAt": @date@,
|
||||||
"translations": {
|
"translations": {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
],
|
],
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
"archivedAt": null,
|
||||||
"createdAt": @date@,
|
"createdAt": @date@,
|
||||||
"updatedAt": @date@,
|
"updatedAt": @date@,
|
||||||
"translations": {
|
"translations": {
|
||||||
|
|
@ -60,6 +61,7 @@
|
||||||
"coupons": [],
|
"coupons": [],
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
"archivedAt": null,
|
||||||
"createdAt": @date@,
|
"createdAt": @date@,
|
||||||
"updatedAt": @date@,
|
"updatedAt": @date@,
|
||||||
"translations": {
|
"translations": {
|
||||||
|
|
@ -75,7 +77,7 @@
|
||||||
"hydra:totalItems": 2,
|
"hydra:totalItems": 2,
|
||||||
"hydra:search": {
|
"hydra:search": {
|
||||||
"@type": "hydra:IriTemplate",
|
"@type": "hydra:IriTemplate",
|
||||||
"hydra:template": "\/api\/v2\/admin\/promotions{?coupons.code,coupons.code[],order[priority]}",
|
"hydra:template": "\/api\/v2\/admin\/promotions{?coupons.code,coupons.code[],order[priority],exists[archivedAt]}",
|
||||||
"hydra:variableRepresentation": "BasicRepresentation",
|
"hydra:variableRepresentation": "BasicRepresentation",
|
||||||
"hydra:mapping": [
|
"hydra:mapping": [
|
||||||
{
|
{
|
||||||
|
|
@ -95,6 +97,12 @@
|
||||||
"variable": "order[priority]",
|
"variable": "order[priority]",
|
||||||
"property": "priority",
|
"property": "priority",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "IriTemplateMapping",
|
||||||
|
"variable": "exists[archivedAt]",
|
||||||
|
"property": "archivedAt",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"exclusive": true,
|
"exclusive": true,
|
||||||
"usageLimit": 3,
|
"usageLimit": 3,
|
||||||
"used": 0,
|
"used": 0,
|
||||||
|
"archivedAt": null,
|
||||||
"startsAt": "2023-10-04 12:30:00",
|
"startsAt": "2023-10-04 12:30:00",
|
||||||
"endsAt": "2023-11-04 12:30:00",
|
"endsAt": "2023-11-04 12:30:00",
|
||||||
"couponBased": true,
|
"couponBased": true,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"exclusive": true,
|
"exclusive": true,
|
||||||
"usageLimit": 11,
|
"usageLimit": 11,
|
||||||
"used": 0,
|
"used": 0,
|
||||||
|
"archivedAt": null,
|
||||||
"startsAt": null,
|
"startsAt": null,
|
||||||
"endsAt": null,
|
"endsAt": null,
|
||||||
"couponBased": false,
|
"couponBased": false,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"code": "50_off",
|
"code": "50_off",
|
||||||
"name": "50% Off on your first order",
|
"name": "50% Off on your first order",
|
||||||
"description": "Get 50% off of your first purchase",
|
"description": "Get 50% off of your first purchase",
|
||||||
"priority": 2,
|
"priority": 3,
|
||||||
"exclusive": true,
|
"exclusive": true,
|
||||||
"usageLimit": 1,
|
"usageLimit": 1,
|
||||||
"used": 0,
|
"used": 0,
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"appliesToDiscounted": false,
|
"appliesToDiscounted": false,
|
||||||
|
"archivedAt": null,
|
||||||
"createdAt": @date@,
|
"createdAt": @date@,
|
||||||
"updatedAt": @date@,
|
"updatedAt": @date@,
|
||||||
"translations": {
|
"translations": {
|
||||||
|
|
|
||||||
31
tests/Api/Responses/admin/promotion/restore_promotion.json
Normal file
31
tests/Api/Responses/admin/promotion/restore_promotion.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"@context": "\/api\/v2\/contexts\/Promotion",
|
||||||
|
"@id": "\/api\/v2\/admin\/promotions\/back_to_school",
|
||||||
|
"@type": "Promotion",
|
||||||
|
"channels": [
|
||||||
|
"\/api\/v2\/admin\/channels\/WEB",
|
||||||
|
"\/api\/v2\/admin\/channels\/MOBILE"
|
||||||
|
],
|
||||||
|
"id": @integer@,
|
||||||
|
"code": "back_to_school",
|
||||||
|
"name": "Back to school",
|
||||||
|
"description": "Get 10% off on all school supplies",
|
||||||
|
"priority": 3,
|
||||||
|
"exclusive": false,
|
||||||
|
"used": 0,
|
||||||
|
"couponBased": false,
|
||||||
|
"coupons": [],
|
||||||
|
"rules": [],
|
||||||
|
"actions": [],
|
||||||
|
"appliesToDiscounted": true,
|
||||||
|
"createdAt": @date@,
|
||||||
|
"updatedAt": @date@,
|
||||||
|
"translations": {
|
||||||
|
"en_US": {
|
||||||
|
"@id": "\/api\/v2\/admin\/promotion-translations\/@integer@",
|
||||||
|
"@type": "PromotionTranslation",
|
||||||
|
"id": @integer@,
|
||||||
|
"label": "Back to school sale!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue