-
Notifications
You must be signed in to change notification settings - Fork 82
IBX-9680: Extending discounts #2936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mnocon
wants to merge
16
commits into
4.6
Choose a base branch
from
extend-discounts
base: 4.6
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
3b566b3
[WIP] Current status
mnocon 740ad1e
PHP & JS CS Fixes
mnocon 752ef12
Started working on Forms
mnocon 85f4fc9
Current status
mnocon 948c730
Current status
mnocon 80a725a
PHPStan is passing
mnocon 98f7569
Conditions ready
mnocon 8b2e417
Conditions refactored
mnocon b574aef
Extending wizard
mnocon c2157dc
FInal touches
mnocon 65f1ce5
Fixes before review
mnocon 1d445f2
PHP & JS CS Fixes
mnocon 4ccd097
Rebuild
mnocon a1a2b34
Review fixes
mnocon 2b7c70c
PHP & JS CS Fixes
mnocon 1f73465
Removed not needed code sample ranges
mnocon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
134 changes: 134 additions & 0 deletions
134
code_samples/discounts/src/Command/OrderPriceCommand.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Command; | ||
|
|
||
| use Exception; | ||
| use Ibexa\Contracts\Core\Repository\PermissionResolver; | ||
| use Ibexa\Contracts\Core\Repository\UserService; | ||
| use Ibexa\Contracts\OrderManagement\OrderServiceInterface; | ||
| use Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface; | ||
| use Ibexa\Contracts\ProductCatalog\PriceResolverInterface; | ||
| use Ibexa\Contracts\ProductCatalog\ProductPriceServiceInterface; | ||
| use Ibexa\Contracts\ProductCatalog\ProductServiceInterface; | ||
| use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContext; | ||
| use Ibexa\Contracts\ProductCatalog\Values\Price\PriceEnvelopeInterface; | ||
| use Ibexa\Discounts\Value\Price\Stamp\DiscountStamp; | ||
| use Ibexa\OrderManagement\Discounts\Value\DiscountsData; | ||
| use Money\Money; | ||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
|
|
||
| final class OrderPriceCommand extends Command | ||
| { | ||
| protected static $defaultName = 'app:discounts:prices'; | ||
|
|
||
| private PermissionResolver $permissionResolver; | ||
|
|
||
| private UserService $userService; | ||
|
|
||
| private ProductServiceInterface $productService; | ||
|
|
||
| private OrderServiceInterface $orderService; | ||
|
|
||
| private ProductPriceServiceInterface $productPriceService; | ||
|
|
||
| private CurrencyServiceInterface $currencyService; | ||
|
|
||
| private PriceResolverInterface $priceResolver; | ||
|
|
||
| public function __construct( | ||
| PermissionResolver $permissionResolver, | ||
| UserService $userService, | ||
| ProductServiceInterface $productService, | ||
| OrderServiceInterface $orderService, | ||
| ProductPriceServiceInterface $productPriceService, | ||
| CurrencyServiceInterface $currencyService, | ||
| PriceResolverInterface $priceResolver | ||
| ) { | ||
| parent::__construct(); | ||
|
|
||
| $this->permissionResolver = $permissionResolver; | ||
| $this->userService = $userService; | ||
| $this->productService = $productService; | ||
| $this->orderService = $orderService; | ||
| $this->productPriceService = $productPriceService; | ||
| $this->currencyService = $currencyService; | ||
| $this->priceResolver = $priceResolver; | ||
| } | ||
|
|
||
| public function execute(InputInterface $input, OutputInterface $output): int | ||
| { | ||
| $this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin')); | ||
|
|
||
| $productCode = 'product_code_control_unit_0'; | ||
| $orderIdentifier = '4315bc58-1e96-4f21-82a0-15f736cbc4bc'; | ||
| $currencyCode = 'EUR'; | ||
|
|
||
| $output->writeln('Product data:'); | ||
| $product = $this->productService->getProduct($productCode); | ||
| $currency = $this->currencyService->getCurrencyByCode($currencyCode); | ||
|
|
||
| $basePrice = $this->productPriceService->getPriceByProductAndCurrency($product, $currency); | ||
| $resolvedPrice = $this->priceResolver->resolvePrice($product, new PriceContext($currency)); | ||
|
|
||
| if ($resolvedPrice === null) { | ||
| throw new Exception('Could not resolve price for the product'); | ||
| } | ||
|
|
||
| $output->writeln(sprintf('Base price: %s', $this->formatPrice($basePrice->getMoney()))); | ||
| $output->writeln(sprintf('Discounted price: %s', $this->formatPrice($resolvedPrice->getMoney()))); | ||
|
|
||
| if ($resolvedPrice instanceof PriceEnvelopeInterface) { | ||
| /** @var \Ibexa\Discounts\Value\Price\Stamp\DiscountStamp $discountStamp */ | ||
| foreach ($resolvedPrice->all(DiscountStamp::class) as $discountStamp) { | ||
| $output->writeln( | ||
| sprintf( | ||
| 'Discount applied: %s , new amount: %s', | ||
| $discountStamp->getDiscount()->getName(), | ||
| $this->formatPrice( | ||
| $discountStamp->getNewPrice() | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| $output->writeln('Order details:'); | ||
|
|
||
| $order = $this->orderService->getOrderByIdentifier($orderIdentifier); | ||
| foreach ($order->getItems() as $item) { | ||
| /** @var ?DiscountsData $discountData */ | ||
| $discountData = $item->getContext()['discount_data'] ?? null; | ||
| if ($discountData instanceof DiscountsData) { | ||
| $output->writeln( | ||
| sprintf( | ||
| 'Product bought with discount: %s, base price: %s, discounted price: %s', | ||
| $item->getProduct()->getName(), | ||
| $this->formatPrice($discountData->getOriginalPrice()), | ||
| $this->formatPrice( | ||
| $item->getValue()->getUnitPriceGross() | ||
| ) | ||
| ) | ||
| ); | ||
| } else { | ||
| $output->writeln( | ||
| sprintf( | ||
| 'Product bought with original price: %s, price: %s', | ||
| $item->getProduct()->getName(), | ||
| $this->formatPrice( | ||
| $item->getValue()->getUnitPriceGross() | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return Command::SUCCESS; | ||
| } | ||
|
|
||
| private function formatPrice(Money $money): string | ||
| { | ||
| return $money->getAmount() / 100.0 . ' ' . $money->getCurrency()->getCode(); | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\Condition; | ||
|
|
||
| use Ibexa\Contracts\Discounts\Value\DiscountConditionInterface; | ||
| use Ibexa\Discounts\Value\AbstractDiscountExpressionAware; | ||
|
|
||
| final class IsAccountAnniversary extends AbstractDiscountExpressionAware implements DiscountConditionInterface | ||
| { | ||
| public const IDENTIFIER = 'is_account_anniversary'; | ||
|
|
||
| public function __construct(?int $tolerance = null) | ||
| { | ||
| parent::__construct([ | ||
| 'tolerance' => $tolerance ?? 0, | ||
| ]); | ||
| } | ||
|
|
||
| public function getTolerance(): int | ||
| { | ||
| return $this->getExpressionValue('tolerance'); | ||
| } | ||
|
|
||
| public function getIdentifier(): string | ||
| { | ||
| return self::IDENTIFIER; | ||
| } | ||
|
|
||
| public function getExpression(): string | ||
| { | ||
| return 'is_anniversary(current_user_registration_date, tolerance)'; | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\Condition; | ||
|
|
||
| use Ibexa\Contracts\Discounts\Value\DiscountConditionInterface; | ||
| use Ibexa\Discounts\Repository\DiscountCondition\DiscountConditionFactoryInterface; | ||
|
|
||
| final class IsAccountAnniversaryConditionFactory implements DiscountConditionFactoryInterface | ||
| { | ||
| public function createDiscountCondition(?array $expressionValues): DiscountConditionInterface | ||
| { | ||
| return new IsAccountAnniversary( | ||
| $expressionValues['tolerance'] ?? null | ||
| ); | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
...amples/discounts/src/Discounts/ExpressionProvider/CurrentUserRegistrationDateResolver.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\ExpressionProvider; | ||
|
|
||
| use Ibexa\Contracts\Core\Repository\PermissionResolver; | ||
| use Ibexa\Contracts\Core\Repository\UserService; | ||
| use Ibexa\Contracts\Discounts\DiscountVariablesResolverInterface; | ||
| use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContextInterface; | ||
|
|
||
| final class CurrentUserRegistrationDateResolver implements DiscountVariablesResolverInterface | ||
| { | ||
| private PermissionResolver $permissionResolver; | ||
|
|
||
| private UserService $userService; | ||
|
|
||
| public function __construct(PermissionResolver $permissionResolver, UserService $userService) | ||
| { | ||
| $this->permissionResolver = $permissionResolver; | ||
| $this->userService = $userService; | ||
| } | ||
|
|
||
| /** | ||
| * @return array{current_user_registration_date: \DateTimeInterface} | ||
| */ | ||
| public function getVariables(PriceContextInterface $priceContext): array | ||
| { | ||
| return [ | ||
| 'current_user_registration_date' => $this->userService->loadUser( | ||
| $this->permissionResolver->getCurrentUserReference()->getUserId() | ||
| )->getContentInfo()->publishedDate, | ||
| ]; | ||
| } | ||
| } | ||
41 changes: 41 additions & 0 deletions
41
code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\ExpressionProvider; | ||
|
|
||
| use DateTimeImmutable; | ||
| use DateTimeInterface; | ||
|
|
||
| final class IsAnniversaryResolver | ||
| { | ||
| private const YEAR_MONTH_DAY_FORMAT = 'Y-m-d'; | ||
|
|
||
| private const MONTH_DAY_FORMAT = 'm-d'; | ||
|
|
||
| private const REFERENCE_YEAR = 2000; | ||
|
|
||
| public function __invoke(DateTimeInterface $date, int $tolerance = 0): bool | ||
| { | ||
| $d1 = $this->unifyYear(new DateTimeImmutable()); | ||
| $d2 = $this->unifyYear($date); | ||
|
|
||
| $diff = $d1->diff($d2, true)->days; | ||
|
|
||
| // Check if the difference between dates is within the tolerance | ||
| return $diff <= $tolerance; | ||
| } | ||
|
|
||
| private function unifyYear(DateTimeInterface $date): DateTimeImmutable | ||
| { | ||
| // Create a new date using the reference year but with the same month and day | ||
| $newDate = DateTimeImmutable::createFromFormat( | ||
| self::YEAR_MONTH_DAY_FORMAT, | ||
| self::REFERENCE_YEAR . '-' . $date->format(self::MONTH_DAY_FORMAT) | ||
| ); | ||
|
Comment on lines
+30
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool concept, but it won't work for leap years - this code breaks on 29th of february :) |
||
|
|
||
| if ($newDate === false) { | ||
| throw new \RuntimeException('Failed to unify year for date.'); | ||
| } | ||
|
|
||
| return $newDate; | ||
| } | ||
| } | ||
24 changes: 24 additions & 0 deletions
24
code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts; | ||
|
|
||
| use Ibexa\Contracts\Discounts\DiscountPrioritizationStrategyInterface; | ||
| use Ibexa\Contracts\Discounts\Value\Query\SortClause\UpdatedAt; | ||
|
|
||
| final class RecentDiscountPrioritizationStrategy implements DiscountPrioritizationStrategyInterface | ||
| { | ||
| private DiscountPrioritizationStrategyInterface $inner; | ||
|
|
||
| public function __construct(DiscountPrioritizationStrategyInterface $inner) | ||
| { | ||
| $this->inner = $inner; | ||
| } | ||
|
|
||
| public function getOrder(): array | ||
| { | ||
| return array_merge( | ||
| [new UpdatedAt()], | ||
| $this->inner->getOrder() | ||
| ); | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\Rule; | ||
|
|
||
| use Ibexa\Contracts\Discounts\DiscountValueFormatterInterface; | ||
| use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface; | ||
| use Money\Money; | ||
|
|
||
| final class PurchaseParityValueFormatter implements DiscountValueFormatterInterface | ||
| { | ||
| public function format(DiscountRuleInterface $discountRule, ?Money $money = null): string | ||
| { | ||
| return 'Regional discount'; | ||
| } | ||
| } |
44 changes: 44 additions & 0 deletions
44
code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRule.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\Rule; | ||
|
|
||
| use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface; | ||
| use Ibexa\Discounts\Value\AbstractDiscountExpressionAware; | ||
|
|
||
| final class PurchasingPowerParityRule extends AbstractDiscountExpressionAware implements DiscountRuleInterface | ||
| { | ||
| public const TYPE = 'purchasing_power_parity'; | ||
|
|
||
| private const DEFAULT_PARITY_MAP = [ | ||
| 'default' => 100, | ||
| 'germany' => 81.6, | ||
| 'france' => 80, | ||
| 'spain' => 69, | ||
| ]; | ||
|
|
||
| /** @param ?array<string, float> $powerParityMap */ | ||
| public function __construct(?array $powerParityMap = null) | ||
| { | ||
| parent::__construct( | ||
| [ | ||
| 'power_parity_map' => $powerParityMap ?? self::DEFAULT_PARITY_MAP, | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /** @return array<string, float> */ | ||
| public function getMap(): array | ||
| { | ||
| return $this->getExpressionValue('power_parity_map'); | ||
| } | ||
|
|
||
| public function getExpression(): string | ||
| { | ||
| return 'amount * (power_parity_map[get_current_region().getIdentifier()] / power_parity_map["default"])'; | ||
| } | ||
|
|
||
| public function getType(): string | ||
| { | ||
| return self::TYPE; | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\Rule; | ||
|
|
||
| use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface; | ||
| use Ibexa\Discounts\Repository\DiscountRule\DiscountRuleFactoryInterface; | ||
|
|
||
| final class PurchasingPowerParityRuleFactory implements DiscountRuleFactoryInterface | ||
| { | ||
| public function createDiscountRule(?array $expressionValues): DiscountRuleInterface | ||
| { | ||
| return new PurchasingPowerParityRule($expressionValues['power_parity_map'] ?? null); | ||
| } | ||
| } |
20 changes: 20 additions & 0 deletions
20
code_samples/discounts/src/Discounts/Step/AnniversaryConditionStep.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace App\Discounts\Step; | ||
|
|
||
| use Ibexa\Contracts\Discounts\Admin\Form\Data\AbstractDiscountStep; | ||
|
|
||
| final class AnniversaryConditionStep extends AbstractDiscountStep | ||
| { | ||
| public const IDENTIFIER = 'anniversary_condition_step'; | ||
|
|
||
| public bool $enabled; | ||
|
|
||
| public int $tolerance; | ||
|
|
||
| public function __construct(bool $enabled = false, int $tolerance = 0) | ||
| { | ||
| $this->enabled = $enabled; | ||
| $this->tolerance = $tolerance; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, whenever additional data might be needed for Discount condition/rule, it is heavily suggested to use data loaded on demand, by a function call.
Providing variable to expression engine means that it will be loaded always, regardless if the variable is actually needed for resolution or not.
I would rather suggest making this a function available within the engine. It could even use caching to ensure it's not loaded more than once.