From 9915ab846446f716fbe3412b157048fed1556e04 Mon Sep 17 00:00:00 2001 From: tleon Date: Wed, 19 Feb 2025 17:22:27 +0100 Subject: [PATCH] chore(cqrs): add new discount level cqrs command --- src/Adapter/CartRule/CartRuleBuilder.php | 6 ++ .../AddCartLevelDiscountHandler.php | 52 +++++++++++ .../GetDiscountForEditingHandler.php | 4 +- .../Command/AddCartLevelDiscountCommand.php | 74 ++++++++++++++++ .../AddCartLevelDiscountHandlerInterface.php | 35 ++++++++ .../QueryResult/DiscountForEditing.php | 12 +++ .../Discount/ValueObject/AmountDiscount.php | 61 +++++++++++++ .../Discount/ValueObject/DiscountType.php | 3 +- .../Discount/ValueObject/PercentDiscount.php | 61 +++++++++++++ .../config/services/adapter/discount.yml | 5 ++ .../Discount/DiscountFeatureContext.php | 41 +++++++++ .../Scenario/Discount/BO/add_discount.feature | 81 +++++++++++++++++ ...user_experience_test_with_discount.feature | 86 +++++++++++++++++++ 13 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 src/Adapter/Discount/CommandHandler/AddCartLevelDiscountHandler.php create mode 100644 src/Core/Domain/Discount/Command/AddCartLevelDiscountCommand.php create mode 100644 src/Core/Domain/Discount/CommandHandler/AddCartLevelDiscountHandlerInterface.php create mode 100644 src/Core/Domain/Discount/ValueObject/AmountDiscount.php create mode 100644 src/Core/Domain/Discount/ValueObject/PercentDiscount.php diff --git a/src/Adapter/CartRule/CartRuleBuilder.php b/src/Adapter/CartRule/CartRuleBuilder.php index 73941be8b300e..ba912932e2bba 100644 --- a/src/Adapter/CartRule/CartRuleBuilder.php +++ b/src/Adapter/CartRule/CartRuleBuilder.php @@ -28,6 +28,7 @@ use CartRule; use DateTimeImmutable; +use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddCartLevelDiscountCommand; use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddDiscountCommand; use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddFreeShippingDiscountCommand; use PrestaShop\PrestaShop\Core\Util\DateTime\DateTime as DateTimeUtil; @@ -55,6 +56,11 @@ public function build(AddDiscountCommand $command): CartRule $cartRule->type = $command->getDiscountType()->getValue(); $cartRule->free_shipping = $command instanceof AddFreeShippingDiscountCommand; + if ($command instanceof AddCartLevelDiscountCommand) { + $cartRule->reduction_percent = (float) (string) $command->getPercentDiscount()?->getValue(); + $cartRule->reduction_amount = (float) (string) $command->getAmountDiscount()?->getValue(); + } + return $cartRule; } } diff --git a/src/Adapter/Discount/CommandHandler/AddCartLevelDiscountHandler.php b/src/Adapter/Discount/CommandHandler/AddCartLevelDiscountHandler.php new file mode 100644 index 0000000000000..034964b188705 --- /dev/null +++ b/src/Adapter/Discount/CommandHandler/AddCartLevelDiscountHandler.php @@ -0,0 +1,52 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\PrestaShop\Adapter\Discount\CommandHandler; + +use PrestaShop\PrestaShop\Adapter\CartRule\CartRuleBuilder; +use PrestaShop\PrestaShop\Adapter\Discount\Repository\DiscountRepository; +use PrestaShop\PrestaShop\Core\CommandBus\Attributes\AsCommandHandler; +use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddCartLevelDiscountCommand; +use PrestaShop\PrestaShop\Core\Domain\Discount\CommandHandler\AddCartLevelDiscountHandlerInterface; +use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountId; + +#[AsCommandHandler] +class AddCartLevelDiscountHandler implements AddCartLevelDiscountHandlerInterface +{ + public function __construct( + private readonly DiscountRepository $discountRepository, + private readonly CartRuleBuilder $cartRuleBuilder + ) { + } + + public function handle(AddCartLevelDiscountCommand $command): DiscountId + { + $BuiltCartRule = $this->cartRuleBuilder->build($command); + $discount = $this->discountRepository->add($BuiltCartRule); + + return new DiscountId((int) $discount->id); + } +} diff --git a/src/Adapter/Discount/QueryHandler/GetDiscountForEditingHandler.php b/src/Adapter/Discount/QueryHandler/GetDiscountForEditingHandler.php index c53fa095d0dfd..8bfd5626f5bce 100644 --- a/src/Adapter/Discount/QueryHandler/GetDiscountForEditingHandler.php +++ b/src/Adapter/Discount/QueryHandler/GetDiscountForEditingHandler.php @@ -62,7 +62,9 @@ public function handle(GetDiscountForEditing $query): DiscountForEditing (int) $cartRule->id_customer, $cartRule->highlight, $cartRule->partial_use, - $cartRule->type + $cartRule->type, + $cartRule->reduction_percent, + $cartRule->reduction_amount ); } } diff --git a/src/Core/Domain/Discount/Command/AddCartLevelDiscountCommand.php b/src/Core/Domain/Discount/Command/AddCartLevelDiscountCommand.php new file mode 100644 index 0000000000000..33fdedb7476b4 --- /dev/null +++ b/src/Core/Domain/Discount/Command/AddCartLevelDiscountCommand.php @@ -0,0 +1,74 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\PrestaShop\Core\Domain\Discount\Command; + +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountConstraintException; +use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\AmountDiscount; +use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountType; +use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\PercentDiscount; + +class AddCartLevelDiscountCommand extends AddDiscountCommand +{ + private ?PercentDiscount $percentDiscount = null; + private ?AmountDiscount $amountDiscount = null; + + public function __construct( + ) { + parent::__construct(DiscountType::CART_DISCOUNT); + } + + public function getPercentDiscount(): ?PercentDiscount + { + return $this->percentDiscount; + } + + /** + * @throws DiscountConstraintException + */ + public function setPercentDiscount(DecimalNumber $percentDiscount): self + { + $this->percentDiscount = new PercentDiscount($percentDiscount); + + return $this; + } + + public function getAmountDiscount(): ?AmountDiscount + { + return $this->amountDiscount; + } + + /** + * @throws DiscountConstraintException + */ + public function setAmountDiscount(DecimalNumber $amountDiscount): self + { + $this->amountDiscount = new AmountDiscount($amountDiscount); + + return $this; + } +} diff --git a/src/Core/Domain/Discount/CommandHandler/AddCartLevelDiscountHandlerInterface.php b/src/Core/Domain/Discount/CommandHandler/AddCartLevelDiscountHandlerInterface.php new file mode 100644 index 0000000000000..27b6f56d428c7 --- /dev/null +++ b/src/Core/Domain/Discount/CommandHandler/AddCartLevelDiscountHandlerInterface.php @@ -0,0 +1,35 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\PrestaShop\Core\Domain\Discount\CommandHandler; + +use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddCartLevelDiscountCommand; +use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountId; + +interface AddCartLevelDiscountHandlerInterface +{ + public function handle(AddCartLevelDiscountCommand $command): DiscountId; +} diff --git a/src/Core/Domain/Discount/QueryResult/DiscountForEditing.php b/src/Core/Domain/Discount/QueryResult/DiscountForEditing.php index a662a791abcbf..7427b6b443e96 100644 --- a/src/Core/Domain/Discount/QueryResult/DiscountForEditing.php +++ b/src/Core/Domain/Discount/QueryResult/DiscountForEditing.php @@ -45,6 +45,8 @@ public function __construct( private readonly bool $highlightInCart, private readonly bool $allowPartialUse, private readonly string $type, + private readonly ?float $percentDiscount, + private readonly ?float $amountDiscount, ) { } @@ -112,4 +114,14 @@ public function getType(): DiscountType { return new DiscountType($this->type); } + + public function getPercentDiscount(): ?float + { + return $this->percentDiscount; + } + + public function getAmountDiscount(): ?float + { + return $this->amountDiscount; + } } diff --git a/src/Core/Domain/Discount/ValueObject/AmountDiscount.php b/src/Core/Domain/Discount/ValueObject/AmountDiscount.php new file mode 100644 index 0000000000000..0f3cbfabba42a --- /dev/null +++ b/src/Core/Domain/Discount/ValueObject/AmountDiscount.php @@ -0,0 +1,61 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject; + +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountConstraintException; + +class AmountDiscount +{ + private DecimalNumber $amount; + + /** + * @throws DiscountConstraintException + */ + public function __construct(DecimalNumber $amount) + { + $this->assertIsPositive($amount); + $this->amount = $amount; + } + + public function getValue(): DecimalNumber + { + return $this->amount; + } + + /** + * @param DecimalNumber $value + * + * @throws DiscountConstraintException + */ + private function assertIsPositive(DecimalNumber $value): void + { + if ($value->isLowerThanZero()) { + throw new DiscountConstraintException(sprintf('Invalid amount reduction "%s".', $value), DiscountConstraintException::INVALID_REDUCTION_AMOUNT); + } + } +} diff --git a/src/Core/Domain/Discount/ValueObject/DiscountType.php b/src/Core/Domain/Discount/ValueObject/DiscountType.php index 8186c62ac5149..031a93ba21b88 100644 --- a/src/Core/Domain/Discount/ValueObject/DiscountType.php +++ b/src/Core/Domain/Discount/ValueObject/DiscountType.php @@ -29,8 +29,7 @@ class DiscountType { public const FREE_SHIPPING = 'free_shipping'; - public const PERCENT_DISCOUNT = 'percent_discount'; - public const AMOUNT_DISCOUNT = 'amount_discount'; + public const CART_DISCOUNT = 'cart_discount'; public const FREE_GIFT = 'free_gift'; public function __construct(private readonly string $value) diff --git a/src/Core/Domain/Discount/ValueObject/PercentDiscount.php b/src/Core/Domain/Discount/ValueObject/PercentDiscount.php new file mode 100644 index 0000000000000..d34f94560ba6f --- /dev/null +++ b/src/Core/Domain/Discount/ValueObject/PercentDiscount.php @@ -0,0 +1,61 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject; + +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountConstraintException; + +class PercentDiscount +{ + private DecimalNumber $percent; + + /** + * @throws DiscountConstraintException + */ + public function __construct(DecimalNumber $percent) + { + $this->assertIsPositive($percent); + $this->percent = $percent; + } + + public function getValue(): DecimalNumber + { + return $this->percent; + } + + /** + * @param DecimalNumber $value + * + * @throws DiscountConstraintException + */ + private function assertIsPositive(DecimalNumber $value): void + { + if ($value->isLowerThanZero()) { + throw new DiscountConstraintException(sprintf('Invalid percent reduction "%s".', $value), DiscountConstraintException::INVALID_REDUCTION_PERCENT); + } + } +} diff --git a/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml b/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml index fcc2372641026..efa6fc9949739 100644 --- a/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml +++ b/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml @@ -7,6 +7,11 @@ services: public: false autoconfigure: true + PrestaShop\PrestaShop\Adapter\Discount\CommandHandler\AddCartLevelDiscountHandler: + autowire: true + public: false + autoconfigure: true + PrestaShop\PrestaShop\Adapter\Discount\QueryHandler\GetDiscountForEditingHandler: autowire: true public: false diff --git a/tests/Integration/Behaviour/Features/Context/Domain/Discount/DiscountFeatureContext.php b/tests/Integration/Behaviour/Features/Context/Domain/Discount/DiscountFeatureContext.php index c38248eaa0e79..16ec02f65250f 100644 --- a/tests/Integration/Behaviour/Features/Context/Domain/Discount/DiscountFeatureContext.php +++ b/tests/Integration/Behaviour/Features/Context/Domain/Discount/DiscountFeatureContext.php @@ -30,6 +30,8 @@ use DateTimeImmutable; use Exception; use PHPUnit\Framework\Assert; +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddCartLevelDiscountCommand; use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddDiscountCommand; use PrestaShop\PrestaShop\Core\Domain\Discount\Command\AddFreeShippingDiscountCommand; use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountConstraintException; @@ -61,6 +63,27 @@ public function createFreeShippingDiscount(string $discountReference, TableNode } } + /** + * @When I create a cart level discount :discountReference with following properties: + * + * @param string $discountReference + * @param TableNode $node + * + * @return void + * + * @throws Exception + */ + public function createCartLevelDiscountIfNotExists(string $discountReference, TableNode $node): void + { + $data = $this->localizeByRows($node); + try { + $command = new AddCartLevelDiscountCommand(); + $this->createDiscount($discountReference, $data, $command); + } catch (DiscountConstraintException $e) { + $this->setLastException($e); + } + } + /** * @When I create a free shipping discount :discountReference * @@ -151,6 +174,16 @@ protected function createDiscount(string $cartRuleReference, array $data, AddDis $command->setCode($data['code']); } + if ($command instanceof AddCartLevelDiscountCommand) { + if (!empty($data['reduction_percent'])) { + $command->setPercentDiscount(new DecimalNumber($data['reduction_percent'])); + } + + if (!empty($data['reduction_amount'])) { + $command->setAmountDiscount(new DecimalNumber($data['reduction_amount'])); + } + } + /** @var DiscountId $discountId */ $discountId = $this->getCommandBus()->handle($command); $this->getSharedStorage()->set($cartRuleReference, $discountId->getValue()); @@ -215,6 +248,14 @@ protected function assertDiscountProperties(DiscountForEditing $discountForEditi if (isset($expectedData['quantity_per_user'])) { Assert::assertSame((int) $expectedData['quantity_per_user'], $discountForEditing->getQuantityPerUser(), 'Unexpected quantity_per_user'); } + + if (isset($expectedData['reduction_percent'])) { + Assert::assertSame((float) $expectedData['reduction_percent'], $discountForEditing->getPercentDiscount()); + } + + if (isset($expectedData['reduction_amount'])) { + Assert::assertSame((float) $expectedData['reduction_amount'], $discountForEditing->getAmountDiscount()); + } } protected function getDiscountForEditing(string $discountReference): DiscountForEditing diff --git a/tests/Integration/Behaviour/Features/Scenario/Discount/BO/add_discount.feature b/tests/Integration/Behaviour/Features/Scenario/Discount/BO/add_discount.feature index 0f9259b1529b4..e2d7153e18a64 100644 --- a/tests/Integration/Behaviour/Features/Scenario/Discount/BO/add_discount.feature +++ b/tests/Integration/Behaviour/Features/Scenario/Discount/BO/add_discount.feature @@ -63,3 +63,84 @@ Feature: Add discount | active | true | | name[en-US] | Promotion | | name[fr-FR] | | + + Scenario: Create a simple cart level discount + When I create a cart level discount "basic_cart_level_discount" with following properties: + | type | percent_discount | + | active | false | + | free_shipping | false | + And discount "basic_cart_level_discount" should have the following properties: + | free_shipping | false | + | active | false | + + Scenario: Create a complete cart level discount + When I create a cart level discount "complete_percent_cart_level_discount" with following properties: + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2019-01-01 11:05:00 | + | valid_to | 2019-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2019 | + | reduction_percent | 10.0 | + And discount "complete_percent_cart_level_discount" should have the following properties: + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2019-01-01 11:05:00 | + | valid_to | 2019-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2019 | + | reduction_percent | 10.0 | + When I create a cart level discount "complete_amount_cart_level_discount" with following properties: + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2019-01-01 11:05:00 | + | valid_to | 2019-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2019_2 | + | reduction_amount | 10.0 | + And discount "complete_amount_cart_level_discount" should have the following properties: + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2019-01-01 11:05:00 | + | valid_to | 2019-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2019_2 | + | reduction_amount | 10.0 | + + Scenario: Create an active cart level discount but without names should be forbidden + When I create a cart level discount "invalid_cart_level_discount" with following properties: + | active | true | + Then I should get error that discount field name is invalid + # Discount online with name only in default language is valid though + When I create a cart level discount "default_language_cart_level_discount" with following properties: + | active | true | + | name[en-US] | Promotion | + | name[fr-FR] | | + Then discount "default_language_cart_level_discount" should have the following properties: + | active | true | + | name[en-US] | Promotion | + | name[fr-FR] | | diff --git a/tests/Integration/Behaviour/Features/Scenario/Discount/FO/full_user_experience_test_with_discount.feature b/tests/Integration/Behaviour/Features/Scenario/Discount/FO/full_user_experience_test_with_discount.feature index ce7a16b6d1356..f46f74cdb1d1f 100644 --- a/tests/Integration/Behaviour/Features/Scenario/Discount/FO/full_user_experience_test_with_discount.feature +++ b/tests/Integration/Behaviour/Features/Scenario/Discount/FO/full_user_experience_test_with_discount.feature @@ -54,3 +54,89 @@ Feature: Full UX discount test And my cart total should be 26.812 tax included When I apply the voucher code "discount_1" Then my cart total should be 19.812 tax included + + Scenario: Create a complete cart level flat discount using new CQRS + When I create a cart level discount "discount_2" with following properties: + | type | amount_discount | + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2025-01-01 11:05:00 | + | valid_to | 2025-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2025 | + | reduction_amount | 10.0 | + And discount "discount_2" should have the following properties: + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2025-01-01 11:05:00 | + | valid_to | 2025-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2025 | + | reduction_amount | 10.0 | + + Scenario: One product in cart, one discount offering only free shipping + Given discount "discount_2" should have the following properties: + | name[en-US] | Promotion | + | priority | 2 | + | free_shipping | true | + | code | PROMO_CART_2025 | + And I add 1 items of product "product1" in my cart + And my cart total shipping fees should be 7.0 tax included + And my cart total should be 26.812 tax included + When I apply the voucher code "discount_2" + Then my cart total should be 16.812 tax included + + Scenario: Create a complete cart level percent discount using new CQRS + When I create a cart level discount "discount_3" with following properties: + | type | percent_discount | + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2025-01-01 11:05:00 | + | valid_to | 2025-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2025_2 | + | reduction_percent | 50.0 | + And discount "discount_3" should have the following properties: + | name[en-US] | Promotion | + | description | Promotion for holidays | + | highlight | false | + | is_active | true | + | allow_partial_use | false | + | priority | 2 | + | valid_from | 2025-01-01 11:05:00 | + | valid_to | 2025-12-01 00:00:00 | + | total_quantity | 10 | + | quantity_per_user | 1 | + | free_shipping | false | + | code | PROMO_CART_2025_2 | + | reduction_percent | 50.0 | + + Scenario: One product in cart, one discount offering only free shipping + Given discount "discount_3" should have the following properties: + | name[en-US] | Promotion | + | priority | 2 | + | free_shipping | true | + | code | PROMO_CART_2025_2 | + And I add 1 items of product "product1" in my cart + And my cart total shipping fees should be 7.0 tax included + And my cart total should be 26.812 tax included + When I apply the voucher code "discount_3" + Then my cart total should be 16.906 tax included