Skip to content

Conversation

@mnocon
Copy link
Contributor

@mnocon mnocon commented Oct 15, 2025

Things added:

  1. Example how to deal with discounts products using the API:
  • resolving prices of discounted products and retrieving the applied discounts
  • retrieving discount information from orders

Preview:

  1. Extending:
  • custom condition
  • custom rule
  • extending the form:
  • adding a custom step and integrating a custom condition
  • adding a custom rule

Previews:

  1. Enhanced price resolving descirption to include discounts
  2. Small changes around the the discount docs, like improving the main discounts card page.

@mnocon mnocon changed the title [WIP] Extending discounts Extending discounts Oct 30, 2025
@mnocon mnocon marked this pull request as ready for review November 27, 2025 08:15
@mnocon mnocon changed the title Extending discounts IBX-9680: Extending discounts Nov 27, 2025
By extending [Discounts](discounts_guide.md), you can increase flexibility and control over how promotions are applied to suit your unique business rules.
Together with the existing [events](event_reference.md) and the [Discounts PHP API](discounts_api.md), extending discounts gives you the ability to cover additional use cases related to selling products.

!!! tip
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Experimental, please let me know what you think - I'm ok with removing this as well

Copy link
Contributor Author

@mnocon mnocon Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to include the code in two shapes - first only for creating form (Step1) and then in its full form (Step2)

The Step1/Step2 can be confusing because it's not related to wizard steps, but to given example steps.

@github-actions
Copy link

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/discounts/src/Command/OrderPriceCommand.php


code_samples/discounts/src/Command/OrderPriceCommand.php

docs/discounts/discounts_api.md@143:``` php hl_lines="72-73 79-80 82-95 99-125"
docs/discounts/discounts_api.md@144:[[= include_file('code_samples/discounts/src/Command/OrderPriceCommand.php') =]]
docs/discounts/discounts_api.md@145:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Command;
004⫶
005⫶use Exception;
006⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
007⫶use Ibexa\Contracts\Core\Repository\UserService;
008⫶use Ibexa\Contracts\OrderManagement\OrderServiceInterface;
009⫶use Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface;
010⫶use Ibexa\Contracts\ProductCatalog\PriceResolverInterface;
011⫶use Ibexa\Contracts\ProductCatalog\ProductPriceServiceInterface;
012⫶use Ibexa\Contracts\ProductCatalog\ProductServiceInterface;
013⫶use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContext;
014⫶use Ibexa\Contracts\ProductCatalog\Values\Price\PriceEnvelopeInterface;
015⫶use Ibexa\Discounts\Value\Price\Stamp\DiscountStamp;
016⫶use Ibexa\OrderManagement\Discounts\Value\DiscountsData;
017⫶use Money\Money;
018⫶use Symfony\Component\Console\Command\Command;
019⫶use Symfony\Component\Console\Input\InputInterface;
020⫶use Symfony\Component\Console\Output\OutputInterface;
021⫶
022⫶final class OrderPriceCommand extends Command
023⫶{
024⫶ protected static $defaultName = 'app:discounts:prices';
025⫶
026⫶ private PermissionResolver $permissionResolver;
027⫶
028⫶ private UserService $userService;
029⫶
030⫶ private ProductServiceInterface $productService;
031⫶
032⫶ private OrderServiceInterface $orderService;
033⫶
034⫶ private ProductPriceServiceInterface $productPriceService;
035⫶
036⫶ private CurrencyServiceInterface $currencyService;
037⫶
038⫶ private PriceResolverInterface $priceResolver;
039⫶
040⫶ public function __construct(
041⫶ PermissionResolver $permissionResolver,
042⫶ UserService $userService,
043⫶ ProductServiceInterface $productService,
044⫶ OrderServiceInterface $orderService,
045⫶ ProductPriceServiceInterface $productPriceService,
046⫶ CurrencyServiceInterface $currencyService,
047⫶ PriceResolverInterface $priceResolver
048⫶ ) {
049⫶ parent::__construct();
050⫶
051⫶ $this->permissionResolver = $permissionResolver;
052⫶ $this->userService = $userService;
053⫶ $this->productService = $productService;
054⫶ $this->orderService = $orderService;
055⫶ $this->productPriceService = $productPriceService;
056⫶ $this->currencyService = $currencyService;
057⫶ $this->priceResolver = $priceResolver;
058⫶ }
059⫶
060⫶ public function execute(InputInterface $input, OutputInterface $output): int
061⫶ {
062⫶ $this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin'));
063⫶
064⫶ $productCode = 'product_code_control_unit_0';
065⫶ $orderIdentifier = '4315bc58-1e96-4f21-82a0-15f736cbc4bc';
066⫶ $currencyCode = 'EUR';
067⫶
068⫶ $output->writeln('Product data:');
069⫶ $product = $this->productService->getProduct($productCode);
070⫶ $currency = $this->currencyService->getCurrencyByCode($currencyCode);
071⫶
072❇️ $basePrice = $this->productPriceService->getPriceByProductAndCurrency($product, $currency);
073❇️ $resolvedPrice = $this->priceResolver->resolvePrice($product, new PriceContext($currency));
074⫶
075⫶ if ($resolvedPrice === null) {
076⫶ throw new Exception('Could not resolve price for the product');
077⫶ }
078⫶
079❇️ $output->writeln(sprintf('Base price: %s', $this->formatPrice($basePrice->getMoney())));
080❇️ $output->writeln(sprintf('Discounted price: %s', $this->formatPrice($resolvedPrice->getMoney())));
081⫶
082❇️ if ($resolvedPrice instanceof PriceEnvelopeInterface) {
083❇️ /** @var \Ibexa\Discounts\Value\Price\Stamp\DiscountStamp $discountStamp */
084❇️ foreach ($resolvedPrice->all(DiscountStamp::class) as $discountStamp) {
085❇️ $output->writeln(
086❇️ sprintf(
087❇️ 'Discount applied: %s , new amount: %s',
088❇️ $discountStamp->getDiscount()->getName(),
089❇️ $this->formatPrice(
090❇️ $discountStamp->getNewPrice()
091❇️ )
092❇️ )
093❇️ );
094❇️ }
095❇️ }
096⫶
097⫶ $output->writeln('Order details:');
098⫶
099❇️ $order = $this->orderService->getOrderByIdentifier($orderIdentifier);
100❇️ foreach ($order->getItems() as $item) {
101❇️ /** @var ?DiscountsData $discountData */
102❇️ $discountData = $item->getContext()['discount_data'] ?? null;
103❇️ if ($discountData instanceof DiscountsData) {
104❇️ $output->writeln(
105❇️ sprintf(
106❇️ 'Product bought with discount: %s, base price: %s, discounted price: %s',
107❇️ $item->getProduct()->getName(),
108❇️ $this->formatPrice($discountData->getOriginalPrice()),
109❇️ $this->formatPrice(
110❇️ $item->getValue()->getUnitPriceGross()
111❇️ )
112❇️ )
113❇️ );
114❇️ } else {
115❇️ $output->writeln(
116❇️ sprintf(
117❇️ 'Product bought with original price: %s, price: %s',
118❇️ $item->getProduct()->getName(),
119❇️ $this->formatPrice(
120❇️ $item->getValue()->getUnitPriceGross()
121❇️ )
122❇️ )
123❇️ );
124❇️ }
125❇️ }
126⫶
127⫶ return Command::SUCCESS;
128⫶ }
129⫶
130⫶ private function formatPrice(Money $money): string
131⫶ {
132⫶ return $money->getAmount() / 100.0 . ' ' . $money->getCurrency()->getCode();
133⫶ }
134⫶}


code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php


code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php

docs/discounts/extend_discounts.md@95:``` php
docs/discounts/extend_discounts.md@96:[[= include_file('code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php') =]]
docs/discounts/extend_discounts.md@97:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\Condition;
004⫶
005⫶use Ibexa\Contracts\Discounts\Value\DiscountConditionInterface;
006⫶use Ibexa\Discounts\Value\AbstractDiscountExpressionAware;
007⫶
008⫶final class IsAccountAnniversary extends AbstractDiscountExpressionAware implements DiscountConditionInterface
009⫶{
010⫶ public const IDENTIFIER = 'is_account_anniversary';
011⫶
012⫶ public function __construct(?int $tolerance = null)
013⫶ {
014⫶ parent::__construct([
015⫶ 'tolerance' => $tolerance ?? 0,
016⫶ ]);
017⫶ }
018⫶
019⫶ public function getTolerance(): int
020⫶ {
021⫶ return $this->getExpressionValue('tolerance');
022⫶ }
023⫶
024⫶ public function getIdentifier(): string
025⫶ {
026⫶ return self::IDENTIFIER;
027⫶ }
028⫶
029⫶ public function getExpression(): string
030⫶ {
031⫶ return 'is_anniversary(current_user_registration_date, tolerance)';
032⫶ }
033⫶}


code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php


code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php

docs/discounts/extend_discounts.md@108:``` php
docs/discounts/extend_discounts.md@109:[[= include_file('code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php') =]]
docs/discounts/extend_discounts.md@110:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\Condition;
004⫶
005⫶use Ibexa\Contracts\Discounts\Value\DiscountConditionInterface;
006⫶use Ibexa\Discounts\Repository\DiscountCondition\DiscountConditionFactoryInterface;
007⫶
008⫶final class IsAccountAnniversaryConditionFactory implements DiscountConditionFactoryInterface
009⫶{
010⫶ public function createDiscountCondition(?array $expressionValues): DiscountConditionInterface
011⫶ {
012⫶ return new IsAccountAnniversary(
013⫶ $expressionValues['tolerance'] ?? null
014⫶ );
015⫶ }
016⫶}


code_samples/discounts/src/Discounts/ExpressionProvider/CurrentUserRegistrationDateResolver.php


code_samples/discounts/src/Discounts/ExpressionProvider/CurrentUserRegistrationDateResolver.php

docs/discounts/extend_discounts.md@55:``` php
docs/discounts/extend_discounts.md@56:[[= include_file('code_samples/discounts/src/Discounts/ExpressionProvider/CurrentUserRegistrationDateResolver.php') =]]
docs/discounts/extend_discounts.md@57:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\ExpressionProvider;
004⫶
005⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
006⫶use Ibexa\Contracts\Core\Repository\UserService;
007⫶use Ibexa\Contracts\Discounts\DiscountVariablesResolverInterface;
008⫶use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContextInterface;
009⫶
010⫶final class CurrentUserRegistrationDateResolver implements DiscountVariablesResolverInterface
011⫶{
012⫶ private PermissionResolver $permissionResolver;
013⫶
014⫶ private UserService $userService;
015⫶
016⫶ public function __construct(PermissionResolver $permissionResolver, UserService $userService)
017⫶ {
018⫶ $this->permissionResolver = $permissionResolver;
019⫶ $this->userService = $userService;
020⫶ }
021⫶
022⫶ /** @return array{current_user_registration_date: \DateTimeInterface}
023⫶ */
024⫶ public function getVariables(PriceContextInterface $priceContext): array
025⫶ {
026⫶ return [
027⫶ 'current_user_registration_date' => $this->userService->loadUser(
028⫶ $this->permissionResolver->getCurrentUserReference()->getUserId()
029⫶ )->getContentInfo()->publishedDate,
030⫶ ];
031⫶ }
032⫶}


code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php


code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php

docs/discounts/extend_discounts.md@72:``` php
docs/discounts/extend_discounts.md@73:[[= include_file('code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php') =]]
docs/discounts/extend_discounts.md@74:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\ExpressionProvider;
004⫶
005⫶use DateTimeImmutable;
006⫶use DateTimeInterface;
007⫶use EzSystems\EzPlatformGraphQL\GraphQL\Mutation\InputHandler\FieldType\Date;
008⫶
009⫶final class IsAnniversaryResolver
010⫶{
011⫶ private const YEAR_MONTH_DAY_FORMAT = 'Y-m-d';
012⫶
013⫶ private const MONTH_DAY_FORMAT = 'm-d';
014⫶
015⫶ private const REFERENCE_YEAR = 2000;
016⫶
017⫶ public function __invoke(DateTimeInterface $date, int $tolerance = 0): bool
018⫶ {
019⫶ $d1 = $this->unifyYear(new DateTimeImmutable());
020⫶ $d2 = $this->unifyYear($date);
021⫶
022⫶ $diff = $d1->diff($d2, true)->days;
023⫶
024⫶ // Check if the difference between dates is within the tolerance
025⫶ return $diff <= $tolerance;
026⫶ }
027⫶
028⫶ private function unifyYear(DateTimeInterface $date): DateTimeImmutable
029⫶ {
030⫶ // Create a new date using the reference year but with the same month and day
031⫶ $date = DateTimeImmutable::createFromFormat(
032⫶ self::YEAR_MONTH_DAY_FORMAT,
033⫶ self::REFERENCE_YEAR . '-' . $date->format(self::MONTH_DAY_FORMAT)
034⫶ );
035⫶
036⫶ if ($date === false) {
037⫶ throw new \RuntimeException('Failed to unify year for date.');
038⫶ }
039⫶
040⫶ return $date;
041⫶ }
042⫶}


code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php


code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php

docs/discounts/extend_discounts.md@183:``` php
docs/discounts/extend_discounts.md@184:[[= include_file('code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php') =]]
docs/discounts/extend_discounts.md@185:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts;
004⫶
005⫶use Ibexa\Contracts\Discounts\DiscountPrioritizationStrategyInterface;
006⫶use Ibexa\Contracts\Discounts\Value\Query\SortClause\UpdatedAt;
007⫶
008⫶final class RecentDiscountPrioritizationStrategy implements DiscountPrioritizationStrategyInterface
009⫶{
010⫶ private DiscountPrioritizationStrategyInterface $inner;
011⫶
012⫶ public function __construct(DiscountPrioritizationStrategyInterface $inner)
013⫶ {
014⫶ $this->inner = $inner;
015⫶ }
016⫶
017⫶ public function getOrder(): array
018⫶ {
019⫶ return array_merge(
020⫶ [new UpdatedAt()],
021⫶ $this->inner->getOrder()
022⫶ );
023⫶ }
024⫶}


code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php


code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php

docs/discounts/extend_discounts.md@165:``` php
docs/discounts/extend_discounts.md@166:[[= include_file('code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php') =]]
docs/discounts/extend_discounts.md@167:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\Discounts\Rule;
006⫶
007⫶use Ibexa\Contracts\Discounts\DiscountValueFormatterInterface;
008⫶use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface;
009⫶use Money\Money;
010⫶
011⫶final class PurchaseParityValueFormatter implements DiscountValueFormatterInterface
012⫶{
013⫶ public function format(DiscountRuleInterface $discountRule, ?Money $money = null): string
014⫶ {
015⫶ return 'Regional discount';
016⫶ }
017⫶}


code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRule.php


code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRule.php

docs/discounts/extend_discounts.md@133:``` php
docs/discounts/extend_discounts.md@134:[[= include_file('code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRule.php', 0, 42) =]]
docs/discounts/extend_discounts.md@135:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\Rule;
004⫶
005⫶use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface;
006⫶use Ibexa\Discounts\Value\AbstractDiscountExpressionAware;
007⫶
008⫶final class PurchasingPowerParityRule extends AbstractDiscountExpressionAware implements DiscountRuleInterface
009⫶{
010⫶ public const TYPE = 'purchasing_power_parity';
011⫶
012⫶ private const DEFAULT_PARITY_MAP = [
013⫶ 'default' => 100,
014⫶ 'germany' => 81.6,
015⫶ 'france' => 80,
016⫶ 'spain' => 69,
017⫶ ];
018⫶
019⫶ /** @param ?array<string, float> $powerParityMap */
020⫶ public function __construct(?array $powerParityMap = null)
021⫶ {
022⫶ parent::__construct(
023⫶ [
024⫶ 'power_parity_map' => $powerParityMap ?? self::DEFAULT_PARITY_MAP,
025⫶ ]
026⫶ );
027⫶ }
028⫶
029⫶ /** @return array<string, float> */
030⫶ public function getMap(): array
031⫶ {
032⫶ return $this->getExpressionValue('power_parity_map');
033⫶ }
034⫶
035⫶ public function getExpression(): string
036⫶ {
037⫶ return 'amount * (power_parity_map[get_current_region().getIdentifier()] / power_parity_map["default"])';
038⫶ }
039⫶
040⫶ public function getType(): string
041⫶ {
042⫶ return self::TYPE;


code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php


code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php

docs/discounts/extend_discounts.md@139:``` php
docs/discounts/extend_discounts.md@140:[[= include_file('code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php', 0, 14) =]]
docs/discounts/extend_discounts.md@141:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\Rule;
004⫶
005⫶use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface;
006⫶use Ibexa\Discounts\Repository\DiscountRule\DiscountRuleFactoryInterface;
007⫶
008⫶final class PurchasingPowerParityRuleFactory implements DiscountRuleFactoryInterface
009⫶{
010⫶ public function createDiscountRule(?array $expressionValues): DiscountRuleInterface
011⫶ {
012⫶ return new PurchasingPowerParityRule($expressionValues['power_parity_map'] ?? null);
013⫶ }
014⫶}


code_samples/discounts/src/Discounts/Step/AnniversaryConditionStep.php


code_samples/discounts/src/Discounts/Step/AnniversaryConditionStep.php

docs/discounts/extend_discounts_wizard.md@46:``` php
docs/discounts/extend_discounts_wizard.md@47:[[= include_file('code_samples/discounts/src/Discounts/Step/AnniversaryConditionStep.php') =]]
docs/discounts/extend_discounts_wizard.md@48:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\Step;
004⫶
005⫶use Ibexa\Contracts\Discounts\Admin\Form\Data\AbstractDiscountStep;
006⫶
007⫶final class AnniversaryConditionStep extends AbstractDiscountStep
008⫶{
009⫶ public const IDENTIFIER = 'anniversary_condition_step';
010⫶
011⫶ public bool $enabled;
012⫶
013⫶ public int $tolerance;
014⫶
015⫶ public function __construct(bool $enabled = false, int $tolerance = 0)
016⫶ {
017⫶ $this->enabled = $enabled;
018⫶ $this->tolerance = $tolerance;
019⫶ }
020⫶}


code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php


code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php

docs/discounts/extend_discounts_wizard.md@79:``` php
docs/discounts/extend_discounts_wizard.md@80:[[= include_file('code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php') =]]
docs/discounts/extend_discounts_wizard.md@81:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Discounts\Step;
004⫶
005⫶use App\Form\Type\AnniversaryConditionStepType;
006⫶use Ibexa\Contracts\Discounts\Admin\Form\Data\DiscountStepData;
007⫶use Ibexa\Contracts\Discounts\Admin\Form\Listener\AbstractStepFormListener;
008⫶use JMS\TranslationBundle\Model\Message;
009⫶use JMS\TranslationBundle\Translation\TranslationContainerInterface;
010⫶use Symfony\Component\Form\Event\PreSetDataEvent;
011⫶use Symfony\Component\Form\FormInterface;
012⫶
013⫶final class AnniversaryConditionStepFormListener extends AbstractStepFormListener implements TranslationContainerInterface
014⫶{
015⫶ public function isDataSupported(DiscountStepData $data): bool
016⫶ {
017⫶ return $data->getStepData() instanceof AnniversaryConditionStep;
018⫶ }
019⫶
020⫶ public function addFields(FormInterface $form, DiscountStepData $data, PreSetDataEvent $event): void
021⫶ {
022⫶ $form->add(
023⫶ 'stepData',
024⫶ AnniversaryConditionStepType::class,
025⫶ [
026⫶ 'label' => false,
027⫶ ]
028⫶ );
029⫶ }
030⫶
031⫶ public static function getTranslationMessages(): array
032⫶ {
033⫶ return [
034⫶ (new Message('discount.step.custom.label', 'discount'))->setDesc('Custom'),
035⫶ ];
036⫶ }
037⫶}


code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php


code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php

docs/discounts/extend_discounts_wizard.md@52:``` php hl_lines="18-19 26-50"
docs/discounts/extend_discounts_wizard.md@53:[[= include_file('code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php') =]]
docs/discounts/extend_discounts_wizard.md@54:```

001⫶<?php
002⫶declare(strict_types=1);
003⫶
004⫶namespace App\Discounts\Step;
005⫶
006⫶use App\Discounts\Condition\IsAccountAnniversary;
007⫶use Ibexa\Contracts\Discounts\Event\CreateFormDataEvent;
008⫶use Ibexa\Contracts\Discounts\Event\MapDiscountToFormDataEvent;
009⫶use Ibexa\Contracts\Discounts\Value\DiscountType;
010⫶use Symfony\Component\EventDispatcher\EventSubscriberInterface;
011⫶use Symfony\Contracts\EventDispatcher\Event;
012⫶
013⫶final class AnniversaryConditionStepEventSubscriber implements EventSubscriberInterface
014⫶{
015⫶ public static function getSubscribedEvents(): array
016⫶ {
017⫶ return [
018❇️ CreateFormDataEvent::class => 'addAnniversaryConditionStep',
019❇️ MapDiscountToFormDataEvent::class => 'addAnniversaryConditionStep',
020⫶ ];
021⫶ }
022⫶
023⫶ /**
024⫶ * @param \Ibexa\Contracts\Discounts\Event\CreateFormDataEvent|\Ibexa\Contracts\Discounts\Event\MapDiscountToFormDataEvent $event
025⫶ */
026❇️ public function addAnniversaryConditionStep(Event $event): void
027❇️ {
028❇️ $data = $event->getData();
029❇️ if ($data->getType() !== DiscountType::CART) {
030❇️ return;
031❇️ }
032❇️
033❇️ /** @var \App\Discounts\Condition\IsAccountAnniversary $discount */
034❇️ $discount = $event instanceof MapDiscountToFormDataEvent ?
035❇️ $event->getDiscount()->getConditionByIdentifier(IsAccountAnniversary::IDENTIFIER) :
036❇️ null;
037❇️
038❇️ $conditionStep = $discount !== null ?
039❇️ new AnniversaryConditionStep(true, $discount->getTolerance()) :
040❇️ new AnniversaryConditionStep();
041❇️
042❇️ $event->setData(
043❇️ $event->getData()->withStep(
044❇️ $conditionStep,
045❇️ AnniversaryConditionStep::IDENTIFIER,
046❇️ 'Anniversary Condition',
047❇️ -45 // Priority
048❇️ )
049❇️ );
050❇️ }
051⫶}


code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php


code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php

docs/discounts/extend_discounts_wizard.md@98:``` php hl_lines="23-24 57-70"
docs/discounts/extend_discounts_wizard.md@99:[[= include_file('code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php') =]]
docs/discounts/extend_discounts_wizard.md@100:```

001⫶<?php
002⫶declare(strict_types=1);
003⫶
004⫶namespace App\Discounts\Step;
005⫶
006⫶use App\Discounts\Condition\IsAccountAnniversary;
007⫶use Ibexa\Contracts\Discounts\Event\CreateDiscountCreateStructEvent;
008⫶use Ibexa\Contracts\Discounts\Event\CreateDiscountUpdateStructEvent;
009⫶use Ibexa\Contracts\Discounts\Event\CreateFormDataEvent;
010⫶use Ibexa\Contracts\Discounts\Event\DiscountStructEventInterface;
011⫶use Ibexa\Contracts\Discounts\Event\MapDiscountToFormDataEvent;
012⫶use Ibexa\Contracts\Discounts\Value\DiscountType;
013⫶use Symfony\Component\EventDispatcher\EventSubscriberInterface;
014⫶use Symfony\Contracts\EventDispatcher\Event;
015⫶
016⫶final class AnniversaryConditionStepEventSubscriber implements EventSubscriberInterface
017⫶{
018⫶ public static function getSubscribedEvents(): array
019⫶ {
020⫶ return [
021⫶ CreateFormDataEvent::class => 'addAnniversaryConditionStep',
022⫶ MapDiscountToFormDataEvent::class => 'addAnniversaryConditionStep',
023❇️ CreateDiscountCreateStructEvent::class => 'addStepDataToStruct',
024❇️ CreateDiscountUpdateStructEvent::class => 'addStepDataToStruct',
025⫶ ];
026⫶ }
027⫶
028⫶ /**
029⫶ * @param \Ibexa\Contracts\Discounts\Event\CreateFormDataEvent|\Ibexa\Contracts\Discounts\Event\MapDiscountToFormDataEvent $event
030⫶ */
031⫶ public function addAnniversaryConditionStep(Event $event): void
032⫶ {
033⫶ $data = $event->getData();
034⫶ if ($data->getType() !== DiscountType::CART) {
035⫶ return;
036⫶ }
037⫶
038⫶ /** @var \App\Discounts\Condition\IsAccountAnniversary $discount */
039⫶ $discount = $event instanceof MapDiscountToFormDataEvent ?
040⫶ $event->getDiscount()->getConditionByIdentifier(IsAccountAnniversary::IDENTIFIER) :
041⫶ null;
042⫶
043⫶ $conditionStep = $discount !== null ?
044⫶ new AnniversaryConditionStep(true, $discount->getTolerance()) :
045⫶ new AnniversaryConditionStep();
046⫶
047⫶ $event->setData(
048⫶ $event->getData()->withStep(
049⫶ $conditionStep,
050⫶ AnniversaryConditionStep::IDENTIFIER,
051⫶ 'Anniversary Condition',
052⫶ -45 // Priority
053⫶ )
054⫶ );
055⫶ }
056⫶
057❇️ public function addStepDataToStruct(DiscountStructEventInterface $event): void
058❇️ {
059❇️ /** @var AnniversaryConditionStep $stepData */
060❇️ $stepData = $event
061❇️ ->getData()
062❇️ ->getStepByIdentifier(AnniversaryConditionStep::IDENTIFIER)?->getStepData();
063❇️
064❇️ if ($stepData === null || !$stepData->enabled) {
065❇️ return;
066❇️ }
067❇️
068❇️ $discountStruct = $event->getStruct();
069❇️ $discountStruct->addCondition(new IsAccountAnniversary($stepData->tolerance));
070❇️ }
071⫶}


code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php


code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php

docs/discounts/extend_discounts_wizard.md@118:``` php
docs/discounts/extend_discounts_wizard.md@119:[[= include_file('code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php') =]]
docs/discounts/extend_discounts_wizard.md@120:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Form\Data;
004⫶
005⫶use Ibexa\Contracts\Discounts\Admin\Form\Data\AbstractDiscountValue;
006⫶
007⫶final class PurchasingPowerParityValue extends AbstractDiscountValue
008⫶{
009⫶ public string $value;
010⫶}


code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityDiscountValueFormTypeMapper.php


code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityDiscountValueFormTypeMapper.php

docs/discounts/extend_discounts_wizard.md@144:``` php
docs/discounts/extend_discounts_wizard.md@145:[[= include_file('code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityDiscountValueFormTypeMapper.php') =]]
docs/discounts/extend_discounts_wizard.md@146:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Form\FormMapper;
004⫶
005⫶use App\Form\Data\PurchasingPowerParityValue;
006⫶use App\Form\Type\DiscountValue\PurchasingPowerParityValueType;
007⫶use Ibexa\Contracts\Discounts\Admin\Form\Data\DiscountValueInterface;
008⫶use Ibexa\Contracts\Discounts\Admin\Form\DiscountValueFormTypeMapperInterface;
009⫶
010⫶final class PurchasingPowerParityDiscountValueFormTypeMapper implements DiscountValueFormTypeMapperInterface
011⫶{
012⫶ public function hasFormTypeForData(DiscountValueInterface $data): bool
013⫶ {
014⫶ return $data instanceof PurchasingPowerParityValue;
015⫶ }
016⫶
017⫶ public function getFormTypeForData(DiscountValueInterface $data): ?string
018⫶ {
019⫶ return $data instanceof PurchasingPowerParityValue ? PurchasingPowerParityValueType::class : null;
020⫶ }
021⫶
022⫶ public function getFormTypeOptionsForData(DiscountValueInterface $data): array
023⫶ {
024⫶ return [];
025⫶ }
026⫶}


code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityFormMapper.php


code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityFormMapper.php

docs/discounts/extend_discounts_wizard.md@124:``` php
docs/discounts/extend_discounts_wizard.md@125:[[= include_file('code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityFormMapper.php') =]]
docs/discounts/extend_discounts_wizard.md@126:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Form\FormMapper;
004⫶
005⫶use App\Discounts\Rule\PurchasingPowerParityRule;
006⫶use Ibexa\Bundle\Discounts\Form\FormMapper\AbstractFormMapper;
007⫶use JMS\TranslationBundle\Model\Message;
008⫶use JMS\TranslationBundle\Translation\TranslationContainerInterface;
009⫶
010⫶final class PurchasingPowerParityFormMapper extends AbstractFormMapper implements TranslationContainerInterface
011⫶{
012⫶ public function getDiscountRuleTypes(?string $type): array
013⫶ {
014⫶ return [PurchasingPowerParityRule::TYPE];
015⫶ }
016⫶
017⫶ public function supports(string $type, string $ruleType): bool
018⫶ {
019⫶ return $ruleType === PurchasingPowerParityRule::TYPE;
020⫶ }
021⫶
022⫶ public static function getTranslationMessages(): array
023⫶ {
024⫶ return [
025⫶ Message::create(
026⫶ sprintf('%s.%s', self::TRANSLATION_PREFIX, PurchasingPowerParityRule::TYPE),
027⫶ 'ibexa_discounts',
028⫶ )->setDesc('Regional'),
029⫶ ];
030⫶ }
031⫶}


code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php


code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php

docs/discounts/extend_discounts_wizard.md@112:``` php hl_lines="59-60"
docs/discounts/extend_discounts_wizard.md@113:[[= include_file('code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php') =]]
docs/discounts/extend_discounts_wizard.md@114:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Form\FormMapper;
004⫶
005⫶use App\Discounts\Rule\PurchasingPowerParityRule;
006⫶use App\Form\Data\PurchasingPowerParityValue;
007⫶use Ibexa\Contracts\Discounts\Admin\Form\Data\DiscountValueInterface;
008⫶use Ibexa\Contracts\Discounts\Admin\FormMapper\DiscountValueMapperInterface;
009⫶use Ibexa\Contracts\Discounts\Value\DiscountInterface;
010⫶use Ibexa\Contracts\Discounts\Value\Struct\DiscountCreateStruct;
011⫶use Ibexa\Contracts\Discounts\Value\Struct\DiscountUpdateStruct;
012⫶use LogicException;
013⫶
014⫶final class PurchasingPowerParityValueMapper implements DiscountValueMapperInterface
015⫶{
016⫶ public function createFormData(string $type, string $ruleType): DiscountValueInterface
017⫶ {
018⫶ if ($ruleType !== PurchasingPowerParityRule::TYPE) {
019⫶ throw new LogicException('Not implemented');
020⫶ }
021⫶
022⫶ return new PurchasingPowerParityValue();
023⫶ }
024⫶
025⫶ public function mapDiscountToFormData(DiscountInterface $discount): DiscountValueInterface
026⫶ {
027⫶ $discountRule = $discount->getRule();
028⫶ if (!$discountRule instanceof PurchasingPowerParityRule) {
029⫶ throw new LogicException('Not implemented');
030⫶ }
031⫶
032⫶ return new PurchasingPowerParityValue();
033⫶ }
034⫶
035⫶ public function mapCreateDataToStruct(
036⫶ DiscountValueInterface $data,
037⫶ DiscountCreateStruct $struct
038⫶ ): void {
039⫶ $this->addRuleToStruct($data, $struct);
040⫶ }
041⫶
042⫶ public function mapUpdateDataToStruct(
043⫶ DiscountInterface $discount,
044⫶ DiscountValueInterface $data,
045⫶ DiscountUpdateStruct $struct
046⫶ ): void {
047⫶ $this->addRuleToStruct($data, $struct);
048⫶ }
049⫶
050⫶ /**
051⫶ * @param \Ibexa\Contracts\Discounts\Value\Struct\DiscountCreateStruct|\Ibexa\Contracts\Discounts\Value\Struct\DiscountUpdateStruct $struct
052⫶ */
053⫶ private function addRuleToStruct(DiscountValueInterface $data, $struct): void
054⫶ {
055⫶ if (!$data instanceof PurchasingPowerParityValue) {
056⫶ throw new LogicException('Not implemented');
057⫶ }
058⫶
059❇️ $rule = new PurchasingPowerParityRule();
060❇️ $struct->setRule($rule);
061⫶ }
062⫶}


code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php


code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php

docs/discounts/extend_discounts_wizard.md@83:``` php
docs/discounts/extend_discounts_wizard.md@84:[[= include_file('code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php') =]]
docs/discounts/extend_discounts_wizard.md@85:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\Form\Type;
006⫶
007⫶use App\Discounts\Step\AnniversaryConditionStep;
008⫶use Symfony\Component\Form\AbstractType;
009⫶use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
010⫶use Symfony\Component\Form\Extension\Core\Type\NumberType;
011⫶use Symfony\Component\Form\FormBuilderInterface;
012⫶use Symfony\Component\OptionsResolver\OptionsResolver;
013⫶
014⫶/**
015⫶ * @extends \Symfony\Component\Form\AbstractType<AnniversaryConditionStep>
016⫶ */
017⫶final class AnniversaryConditionStepType extends AbstractType
018⫶{
019⫶ public function buildForm(FormBuilderInterface $builder, array $options): void
020⫶ {
021⫶ $builder->add(
022⫶ 'enabled',
023⫶ CheckboxType::class,
024⫶ [
025⫶ 'label' => 'Enable anniversary discount',
026⫶ 'required' => false,
027⫶ ]
028⫶ )->add(
029⫶ 'tolerance',
030⫶ NumberType::class,
031⫶ [
032⫶ 'label' => 'Tolerance in days',
033⫶ 'required' => false,
034⫶ ]
035⫶ );
036⫶ }
037⫶
038⫶ public function configureOptions(OptionsResolver $resolver): void
039⫶ {
040⫶ $resolver->setDefaults([
041⫶ 'data_class' => AnniversaryConditionStep::class,
042⫶ ]);
043⫶ }
044⫶}


code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php


code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php

docs/discounts/extend_discounts_wizard.md@150:``` php hl_lines="26-40 47-61 72"
docs/discounts/extend_discounts_wizard.md@151:[[= include_file('code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php') =]]
docs/discounts/extend_discounts_wizard.md@152:```

001⫶<?php
002⫶declare(strict_types=1);
003⫶
004⫶namespace App\Form\Type\DiscountValue;
005⫶
006⫶use App\Form\Data\PurchasingPowerParityValue;
007⫶use Ibexa\Bundle\Discounts\Form\Type\DiscountValueType;
008⫶use Ibexa\Contracts\ProductCatalog\Values\RegionInterface;
009⫶use Symfony\Component\Form\AbstractType;
010⫶use Symfony\Component\Form\Event\PostSubmitEvent;
011⫶use Symfony\Component\Form\Event\PreSetDataEvent;
012⫶use Symfony\Component\Form\Extension\Core\Type\FormType;
013⫶use Symfony\Component\Form\Extension\Core\Type\TextType;
014⫶use Symfony\Component\Form\FormBuilderInterface;
015⫶use Symfony\Component\Form\FormEvents;
016⫶use Symfony\Component\Form\FormInterface;
017⫶use Symfony\Component\OptionsResolver\OptionsResolver;
018⫶
019⫶/**
020⫶ * @extends \Symfony\Component\Form\AbstractType<\App\Form\Data\PurchasingPowerParityValue>
021⫶ */
022⫶final class PurchasingPowerParityValueType extends AbstractType
023⫶{
024⫶ public function buildForm(FormBuilderInterface $builder, array $options): void
025⫶ {
026❇️ $availableRegionHandler = static function (FormInterface $form, PurchasingPowerParityValue $data): void {
027❇️ $regions = $data->getDiscountData()->getGeneralProperties()->getRegions();
028❇️ $regionNames = implode(', ', array_map(static function (RegionInterface $region): string {
029❇️ return $region->getIdentifier();
030❇️ }, $regions));
031❇️
032❇️ $options = [
033❇️ 'required' => false,
034❇️ 'disabled' => true,
035❇️ 'label' => 'This discount applies to the following regions',
036❇️ 'data' => $regionNames,
037❇️ ];
038❇️
039❇️ $form->add('value', TextType::class, $options);
040❇️ };
041⫶
042⫶ $builder->add('type', FormType::class, [
043⫶ 'mapped' => false,
044⫶ 'label' => false,
045⫶ ]);
046⫶
047❇️ $builder->addEventListener(
048❇️ FormEvents::PRE_SET_DATA,
049❇️ static function (PreSetDataEvent $event) use ($availableRegionHandler): void {
050❇️ $form = $event->getForm();
051❇️ $availableRegionHandler($form, $event->getData());
052❇️ },
053❇️ );
054❇️ $builder->get('type')->addEventListener(
055❇️ FormEvents::POST_SUBMIT,
056❇️ static function (PostSubmitEvent $event) use ($availableRegionHandler): void {
057❇️ $form = $event->getForm()->getParent();
058❇️ assert($form !== null);
059❇️ $availableRegionHandler($form, $form->getData());
060❇️ },
061❇️ );
062⫶ }
063⫶
064⫶ public function getParent(): string
065⫶ {
066⫶ return DiscountValueType::class;
067⫶ }
068⫶
069⫶ public function configureOptions(OptionsResolver $resolver): void
070⫶ {
071⫶ $resolver->setDefaults([
072❇️ 'data_class' => PurchasingPowerParityValue::class,
073⫶ ]);
074⫶ }
075⫶}

Download colorized diff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant