diff --git a/code_samples/discounts/src/Command/OrderPriceCommand.php b/code_samples/discounts/src/Command/OrderPriceCommand.php new file mode 100644 index 0000000000..635be4cff1 --- /dev/null +++ b/code_samples/discounts/src/Command/OrderPriceCommand.php @@ -0,0 +1,134 @@ +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(); + } +} diff --git a/code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php b/code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php new file mode 100644 index 0000000000..66c442352a --- /dev/null +++ b/code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php @@ -0,0 +1,33 @@ + $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)'; + } +} diff --git a/code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php b/code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php new file mode 100644 index 0000000000..4d2ede841e --- /dev/null +++ b/code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php @@ -0,0 +1,16 @@ +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, + ]; + } +} diff --git a/code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php b/code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php new file mode 100644 index 0000000000..a430f6dd51 --- /dev/null +++ b/code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php @@ -0,0 +1,42 @@ +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 + $date = DateTimeImmutable::createFromFormat( + self::YEAR_MONTH_DAY_FORMAT, + self::REFERENCE_YEAR . '-' . $date->format(self::MONTH_DAY_FORMAT) + ); + + if ($date === false) { + throw new \RuntimeException('Failed to unify year for date.'); + } + + return $date; + } +} diff --git a/code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php b/code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php new file mode 100644 index 0000000000..490788ef87 --- /dev/null +++ b/code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php @@ -0,0 +1,24 @@ +inner = $inner; + } + + public function getOrder(): array + { + return array_merge( + [new UpdatedAt()], + $this->inner->getOrder() + ); + } +} diff --git a/code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php b/code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php new file mode 100644 index 0000000000..b569b187cb --- /dev/null +++ b/code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php @@ -0,0 +1,17 @@ + 100, + 'germany' => 81.6, + 'france' => 80, + 'spain' => 69, + ]; + + /** @param ?array $powerParityMap */ + public function __construct(?array $powerParityMap = null) + { + parent::__construct( + [ + 'power_parity_map' => $powerParityMap ?? self::DEFAULT_PARITY_MAP, + ] + ); + } + + /** @return array */ + 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; + } +} diff --git a/code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php b/code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php new file mode 100644 index 0000000000..a173345d01 --- /dev/null +++ b/code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php @@ -0,0 +1,14 @@ +enabled = $enabled; + $this->tolerance = $tolerance; + } +} diff --git a/code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php b/code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php new file mode 100644 index 0000000000..0d23b7f7e9 --- /dev/null +++ b/code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php @@ -0,0 +1,37 @@ +getStepData() instanceof AnniversaryConditionStep; + } + + public function addFields(FormInterface $form, DiscountStepData $data, PreSetDataEvent $event): void + { + $form->add( + 'stepData', + AnniversaryConditionStepType::class, + [ + 'label' => false, + ] + ); + } + + public static function getTranslationMessages(): array + { + return [ + (new Message('discount.step.custom.label', 'discount'))->setDesc('Custom'), + ]; + } +} diff --git a/code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php b/code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php new file mode 100644 index 0000000000..8765383d45 --- /dev/null +++ b/code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php @@ -0,0 +1,51 @@ + 'addAnniversaryConditionStep', + MapDiscountToFormDataEvent::class => 'addAnniversaryConditionStep', + ]; + } + + /** + * @param \Ibexa\Contracts\Discounts\Event\CreateFormDataEvent|\Ibexa\Contracts\Discounts\Event\MapDiscountToFormDataEvent $event + */ + public function addAnniversaryConditionStep(Event $event): void + { + $data = $event->getData(); + if ($data->getType() !== DiscountType::CART) { + return; + } + + /** @var \App\Discounts\Condition\IsAccountAnniversary $discount */ + $discount = $event instanceof MapDiscountToFormDataEvent ? + $event->getDiscount()->getConditionByIdentifier(IsAccountAnniversary::IDENTIFIER) : + null; + + $conditionStep = $discount !== null ? + new AnniversaryConditionStep(true, $discount->getTolerance()) : + new AnniversaryConditionStep(); + + $event->setData( + $event->getData()->withStep( + $conditionStep, + AnniversaryConditionStep::IDENTIFIER, + 'Anniversary Condition', + -45 // Priority + ) + ); + } +} diff --git a/code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php b/code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php new file mode 100644 index 0000000000..8f60118ad1 --- /dev/null +++ b/code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php @@ -0,0 +1,71 @@ + 'addAnniversaryConditionStep', + MapDiscountToFormDataEvent::class => 'addAnniversaryConditionStep', + CreateDiscountCreateStructEvent::class => 'addStepDataToStruct', + CreateDiscountUpdateStructEvent::class => 'addStepDataToStruct', + ]; + } + + /** + * @param \Ibexa\Contracts\Discounts\Event\CreateFormDataEvent|\Ibexa\Contracts\Discounts\Event\MapDiscountToFormDataEvent $event + */ + public function addAnniversaryConditionStep(Event $event): void + { + $data = $event->getData(); + if ($data->getType() !== DiscountType::CART) { + return; + } + + /** @var \App\Discounts\Condition\IsAccountAnniversary $discount */ + $discount = $event instanceof MapDiscountToFormDataEvent ? + $event->getDiscount()->getConditionByIdentifier(IsAccountAnniversary::IDENTIFIER) : + null; + + $conditionStep = $discount !== null ? + new AnniversaryConditionStep(true, $discount->getTolerance()) : + new AnniversaryConditionStep(); + + $event->setData( + $event->getData()->withStep( + $conditionStep, + AnniversaryConditionStep::IDENTIFIER, + 'Anniversary Condition', + -45 // Priority + ) + ); + } + + public function addStepDataToStruct(DiscountStructEventInterface $event): void + { + /** @var AnniversaryConditionStep $stepData */ + $stepData = $event + ->getData() + ->getStepByIdentifier(AnniversaryConditionStep::IDENTIFIER)?->getStepData(); + + if ($stepData === null || !$stepData->enabled) { + return; + } + + $discountStruct = $event->getStruct(); + $discountStruct->addCondition(new IsAccountAnniversary($stepData->tolerance)); + } +} diff --git a/code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php b/code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php new file mode 100644 index 0000000000..bf7f19ec2d --- /dev/null +++ b/code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php @@ -0,0 +1,10 @@ +setDesc('Regional'), + ]; + } +} diff --git a/code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php b/code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php new file mode 100644 index 0000000000..50a51c63e7 --- /dev/null +++ b/code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php @@ -0,0 +1,62 @@ +getRule(); + if (!$discountRule instanceof PurchasingPowerParityRule) { + throw new LogicException('Not implemented'); + } + + return new PurchasingPowerParityValue(); + } + + public function mapCreateDataToStruct( + DiscountValueInterface $data, + DiscountCreateStruct $struct + ): void { + $this->addRuleToStruct($data, $struct); + } + + public function mapUpdateDataToStruct( + DiscountInterface $discount, + DiscountValueInterface $data, + DiscountUpdateStruct $struct + ): void { + $this->addRuleToStruct($data, $struct); + } + + /** + * @param \Ibexa\Contracts\Discounts\Value\Struct\DiscountCreateStruct|\Ibexa\Contracts\Discounts\Value\Struct\DiscountUpdateStruct $struct + */ + private function addRuleToStruct(DiscountValueInterface $data, $struct): void + { + if (!$data instanceof PurchasingPowerParityValue) { + throw new LogicException('Not implemented'); + } + + $rule = new PurchasingPowerParityRule(); + $struct->setRule($rule); + } +} diff --git a/code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php b/code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php new file mode 100644 index 0000000000..004d1d1a7f --- /dev/null +++ b/code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php @@ -0,0 +1,44 @@ + + */ +final class AnniversaryConditionStepType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add( + 'enabled', + CheckboxType::class, + [ + 'label' => 'Enable anniversary discount', + 'required' => false, + ] + )->add( + 'tolerance', + NumberType::class, + [ + 'label' => 'Tolerance in days', + 'required' => false, + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => AnniversaryConditionStep::class, + ]); + } +} diff --git a/code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php b/code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php new file mode 100644 index 0000000000..cb998d90b2 --- /dev/null +++ b/code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php @@ -0,0 +1,75 @@ + + */ +final class PurchasingPowerParityValueType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $availableRegionHandler = static function (FormInterface $form, PurchasingPowerParityValue $data): void { + $regions = $data->getDiscountData()->getGeneralProperties()->getRegions(); + $regionNames = implode(', ', array_map(static function (RegionInterface $region): string { + return $region->getIdentifier(); + }, $regions)); + + $options = [ + 'required' => false, + 'disabled' => true, + 'label' => 'This discount applies to the following regions', + 'data' => $regionNames, + ]; + + $form->add('value', TextType::class, $options); + }; + + $builder->add('type', FormType::class, [ + 'mapped' => false, + 'label' => false, + ]); + + $builder->addEventListener( + FormEvents::PRE_SET_DATA, + static function (PreSetDataEvent $event) use ($availableRegionHandler): void { + $form = $event->getForm(); + $availableRegionHandler($form, $event->getData()); + }, + ); + $builder->get('type')->addEventListener( + FormEvents::POST_SUBMIT, + static function (PostSubmitEvent $event) use ($availableRegionHandler): void { + $form = $event->getForm()->getParent(); + assert($form !== null); + $availableRegionHandler($form, $form->getData()); + }, + ); + } + + public function getParent(): string + { + return DiscountValueType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => PurchasingPowerParityValue::class, + ]); + } +} diff --git a/docs/api/event_reference/discounts_events.md b/docs/api/event_reference/discounts_events.md index acfe8c9686..b0eec55ca1 100644 --- a/docs/api/event_reference/discounts_events.md +++ b/docs/api/event_reference/discounts_events.md @@ -11,7 +11,7 @@ month_change: true ## Discount management -The events below are dispatched when managing [`discounts`](discounts_guide.md): +The events below are dispatched when managing [discounts](discounts.md): | Event | Dispatched by | |---|---| @@ -28,7 +28,7 @@ The events below are dispatched when managing [`discounts`](discounts_guide.md): ### Form -The events below allow you to customize the discounts creation wizard: +The events below allow you to [customize the discounts creation wizard](extend_discounts_wizard.md). | Event | Dispatched by | |---|---| @@ -51,6 +51,8 @@ The following events are dispatched when rendering each step of the discount wiz The event classes are shared between steps, but they are dispatched with different names. Each step form mapper dispatches its own set of events. +You can use the names specified above or generate them using the `createEventName` method, for example `CreateFormDataEvent::createEventName(GeneralPropertiesInterface::IDENTIFIER)` returns `ibexa.discounts.form_mapper.general_properties.create_form_data`. + | Form mapper | Step identifier | |---|---| | [`ConditionsMapperInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Admin-FormMapper-ConditionsMapperInterface.html)| [`conditions`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Admin-Form-Data-ConditionsInterface.html#constant_IDENTIFIER) | @@ -73,4 +75,4 @@ The event below allows you to inject your custom logic before the discount code | Event | Dispatched by | Description | |---|---|---| -|[`BeforeDiscountCodeApplyEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-DiscountsCodes-Event-BeforeDiscountCodeApplyEvent.html)|`Ibexa\Bundle\DiscountsCodes\Controller\REST\DiscountCodeController`| Dispatched before a discount code is applied in the cart | +|[`BeforeDiscountCodeApplyEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-DiscountsCodes-Event-BeforeDiscountCodeApplyEvent.html)|`Ibexa\Bundle\DiscountsCodes\Controller\REST\DiscountCodeController`| Dispatched before a discount code is applied in the cart | diff --git a/docs/commerce/order_management/order_management_api.md b/docs/commerce/order_management/order_management_api.md index ed1afc7d6f..bca070cbdb 100644 --- a/docs/commerce/order_management/order_management_api.md +++ b/docs/commerce/order_management/order_management_api.md @@ -22,6 +22,10 @@ To access a single order by using its string identifier, use the [`OrderServiceI [[= include_file('code_samples/api/commerce/src/Command/OrderCommand.php', 57, 61) =]] ``` +Use the returned [`OrderInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-OrderManagement-Value-Order-OrderInterface.html) value object to access details about the order. + +See the [Discounts API](discounts_api.md#retrieve-applied-discounts) to learn how to retrieve applied discount details from the order's context. + ### Get single order by ID To access a single order by using its numerical ID, use the [`OrderServiceInterface::getOrder`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-OrderManagement-OrderServiceInterface.html#method_getOrder) method: diff --git a/docs/discounts/discounts.md b/docs/discounts/discounts.md index aa3d142dcc..72cc2d50b0 100644 --- a/docs/discounts/discounts.md +++ b/docs/discounts/discounts.md @@ -14,9 +14,26 @@ After you install it, temporary or permanent discounts can be applied against it You can also extend the feature, for example, by creating custom pricing rules, application conditions, or changing discount priorities. +## Getting Started + [[= cards([ "discounts/discounts_guide", "discounts/install_discounts", "discounts/configure_discounts", -"discounts/discounts_api" +("permissions/policies#discounts", "Policies", "Learn about the available Discounts policies"), +("https://doc.ibexa.co/projects/userguide/en/4.6/commerce/discounts/work_with_discounts/"), ], columns=2) =]] + +## Development + +[[= cards([ +"discounts/discounts_api", +"discounts/extend_discounts", +"discounts/extend_discounts_wizard", +"api/event_reference/discounts_events", +("https://doc.ibexa.co/en/4.6/api/rest_api/rest_api_reference/rest_api_reference.html#discounts", "REST API Reference", "See the available endpoints for Discounts"), +"templating/twig_function_reference/discounts_twig_functions", +"search/discounts_search_reference/discounts_criteria", +"search/discounts_search_reference/discounts_sort_clauses", +("content_management/data_migration/importing_data#discounts", "Importing Discounts", "Learn how to manage Discounts using data migrations"), +], columns=4) =]] diff --git a/docs/discounts/discounts_api.md b/docs/discounts/discounts_api.md index 73d6ff368d..d8b1e07952 100644 --- a/docs/discounts/discounts_api.md +++ b/docs/discounts/discounts_api.md @@ -36,29 +36,29 @@ Use the expression values provided below when using data migrations or when pars ### Rules Discount rules define how the calculate the price reduction. -The following discount rule types are available: +The following discount rule types are available in the `\Ibexa\Discounts\Value\DiscountRule` namespace: | Rule type | Identifier | Description | Expression value | |---|---|---|---| -| `Ibexa\Discounts\Value\DiscountRule\FixedAmount` | `fixed_amount` | Deducts the specified amount, for example 10 EUR, from the base price | `discount_amount` | -| `Ibexa\Discounts\Value\DiscountRule\Percentage` | `percentage` | Deducts the specified percentage, for example -10%, from the base price | `discount_percentage` | +| `FixedAmount` | `fixed_amount` | Deducts the specified amount, for example 10 EUR, from the base price | `discount_amount` | +| `Percentage` | `percentage` | Deducts the specified percentage, for example -10%, from the base price | `discount_percentage` | Only a single discount can be applied to a given product, and a discount can only have a single rule. ### Conditions -With conditions you can narrow down the scenarios in which the discount applies. The following conditions are available: +With conditions you can narrow down the scenarios in which the discount applies. The following conditions are available in the `\Ibexa\Discounts\Value\DiscountCondition` and `\Ibexa\DiscountsCodes\Value\DiscountCondition` namespaces: | Condition | Applies to | Identifier | Description | Expression values | |---|---|---|---|---| -| `Ibexa\Discounts\Value\DiscountCondition\IsInCategory` | Cart, Catalog | `is_in_category` | Checks if the product belongs to specified [product categories]([[= user_doc =]]/pim/work_with_product_categories) | `categories` | -| `Ibexa\Discounts\Value\DiscountCondition\IsInCurrency` | Cart, Catalog |`is_in_currency` | Checks if the product has price in the specified currency | `currency_code` | -| `Ibexa\Discounts\Value\DiscountCondition\IsInRegions` | Cart, Catalog | `is_in_regions` | Checks if the customer is making the purchase in one of the specified regions | `regions` | -| `Ibexa\Discounts\Value\DiscountCondition\IsProductInArray` | Cart, Catalog| `is_product_in_array` | Checks if the product belongs to the group of selected products | `product_codes` | -| `Ibexa\Discounts\Value\DiscountCondition\IsUserInCustomerGroup` | Cart, Catalog| `is_user_in_customer_group` | Check if the customer belongs to specified [customer groups](customer_groups.md) | `customer_groups` | -| `Ibexa\Discounts\Value\DiscountCondition\IsProductInQuantityInCart` | Cart | `is_product_in_quantity_in_cart` | Checks if the required minimum quantity of a given product is present in the cart | `quantity` | -| `Ibexa\Discounts\Value\DiscountCondition\MinimumPurchaseAmount` | Cart | `minimum_purchase_amount` | Checks if purchase amount in the cart exceeds the specified minimum | `minimum_purchase_amount` | -| `Ibexa\DiscountsCodes\Value\DiscountCondition\IsValidDiscountCode` | Cart | `is_valid_discount_code` | Checks if the correct discount code has been provided and how many times it was used by the customer | `discount_code`, `usage_count` | +| `IsInCategory` | Cart, Catalog | `is_in_category` | Checks if the product belongs to specified [product categories]([[= user_doc =]]/pim/work_with_product_categories) | `categories` | +| `IsInCurrency` | Cart, Catalog |`is_in_currency` | Checks if the product has price in the specified currency | `currency_code` | +| `IsInRegions` | Cart, Catalog | `is_in_regions` | Checks if the customer is making the purchase in one of the specified regions | `regions` | +| `IsProductInArray` | Cart, Catalog | `is_product_in_array` | Checks if the product belongs to the group of selected products | `product_codes` | +| `IsUserInCustomerGroup` | Cart, Catalog| `is_user_in_customer_group` | Check if the customer belongs to specified [customer groups](customer_groups.md) | `customer_groups` | +| `IsProductInQuantityInCart` | Cart | `is_product_in_quantity_in_cart` | Checks if the required minimum quantity of a given product is present in the cart | `quantity` | +| `MinimumPurchaseAmount` | Cart | `minimum_purchase_amount` | Checks if purchase amount in the cart exceeds the specified minimum | `minimum_purchase_amount` | +| `IsValidDiscountCode` | Cart | `is_valid_discount_code` | Checks if the correct discount code has been provided and how many times it was used by the customer | `discount_code`, `usage_count` | When multiple conditions are specified, all of them must be met. @@ -127,3 +127,19 @@ You can search for Discounts using the [`DiscountServiceInterface::findDiscounts To learn more about the available search options, see Discounts' [Search Criteria](discounts_criteria.md) and [Sort Clauses](discounts_sort_clauses.md). For discount codes, you can query the database for discount code usage using [`DiscountCodeServiceInterface::findCodeUsages()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-DiscountsCodes-DiscountCodeServiceInterface.html#method_findCodeUsages) and [`DiscountCodeUsageQuery`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-DiscountsCodes-Value-Query-DiscountCodeUsageQuery.html). + +## Retrieve applied discounts + +The applied discounts change final product pricing. +To learn more about working with prices, see [Price API](price_api.md#prices). + +The example below shows how you can use: + +- [`ProductPriceServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductPriceServiceInterface.html) to query for base product prices +- [`PriceResolverInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-PriceResolverInterface.html) to query for final product prices +- [`PriceEnvelopeInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Price-PriceEnvelopeInterface.html) to retrieve applied discounts +- [`OrderServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-OrderManagement-OrderServiceInterface.html) to display discount details for [orders](order_management.md) + +``` php hl_lines="72-73 79-80 82-95 99-125" +[[= include_file('code_samples/discounts/src/Command/OrderPriceCommand.php') =]] +``` diff --git a/docs/discounts/extend_discounts.md b/docs/discounts/extend_discounts.md new file mode 100644 index 0000000000..2abeb51c11 --- /dev/null +++ b/docs/discounts/extend_discounts.md @@ -0,0 +1,192 @@ +--- +description: Extend Discounts by adding your own rules and conditions +editions: + - lts-update + - commerce +month_change: true +--- + +# Extend Discounts + +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 + + If you prefer learning from videos, two presentations from Ibexa Summit 2025 cover the Discounts feature: + + - Konrad Oboza: [Introduction to the Discounts system in Ibexa DXP](https://www.youtube.com/watch?v=kTgtxY38srw) + - Paweł Niedzielski: [Extending new Discounts to suit your needs](https://www.youtube.com/watch?v=pDJxEKJLwPs) + +## Create custom conditions and rules + +With custom [conditions](discounts_api.md#conditions) you can create more advanced discounts that apply only in specific scenarios. + +The logic for both the conditions and rules is specified using [Symfony's expression language](https://symfony.com/doc/current/components/expression_language.html). + +### Available expressions + +The following expressions are available for conditions and rules: + +| Type | Name | Value | Available for | +| --- | --- | --- | --- | +| Function | `get_current_region()` | [Region object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-RegionInterface.html) of the current siteaccess.| Conditions, rules | +| Function | `is_in_category()` | `true/false`, depending if a product belongs to given [product categories](pim_guide.md#product-categories).| Conditions, rules | +| Function | `is_user_in_customer_group()` | `true/false`, depending if an user belongs to given [customer groups](customer_groups.md). | Conditions, rules | +| Function | `calculate_purchase_amount()` | Purchase amount, calculated for all products in the cart before the discounts are applied.| Conditions, rules | +| Function | `is_product_in_product_codes()` | `true/false`, depending if the product is part of the given list.| Conditions, rules | +| Variable | `cart` | [Cart object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cart-Value-CartInterface.html) associated with current context.| Conditions, rules | +| Variable | `currency` | [Currency object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-CurrencyInterface.html) of the current siteaccess. | Conditions, rules | +| Variable | `customer_group` | [Customer group object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-CustomerGroupInterface.html) associated with given price context or the current user.| Conditions, rules | +| Variable | `amount` | Original price of the product | Rules | +| Variable | `product` | [Product object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductInterface.html)| Rules | + +### Custom expressions + +You can create your own variables and functions to make creating the conditions easier. +The examples below show how to add an additional variable and a function to the available ones: + +- New variable: `current_user_registration_date` + +It's a [`DateTime`](https://www.php.net/manual/en/class.datetime.php) object with the registration date of the currently logged-in user. + +To add it, create a class implementing the [`DiscountVariablesResolverInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountVariablesResolverInterface.html): + +``` php +[[= include_file('code_samples/discounts/src/Discounts/ExpressionProvider/CurrentUserRegistrationDateResolver.php') =]] +``` + +And mark it as a service using the `ibexa.discounts.expression_language.variable_resolver` service tag: + +``` yaml + App\Discounts\ExpressionProvider\CurrentUserRegistrationDateResolver: + tags: + - ibexa.discounts.expression_language.variable_resolver +``` + +- New function: `is_anniversary()` + +It's a function returning a boolean value indicating if today is the anniversary of the date passed as an argument. +The function accepts an optional argument, `tolerance`, allowing you to extend the range of dates that are acccepted as anniversaries. + +``` php +[[= include_file('code_samples/discounts/src/Discounts/ExpressionProvider/IsAnniversaryResolver.php') =]] +``` + +Mark it as a service using the `ibexa.discounts.expression_language.function` service tag and specify the function name in the service definition. + +``` yaml + App\Discounts\ExpressionProvider\IsAnniversaryResolver: + tags: + - name: ibexa.discounts.expression_language.function + function: is_anniversary +``` + +Two new expressions are now available for use in custom conditions and rules. + +### Implement custom condition + +The following example creates a new discount condition. It allows you to offer a special discount for customers on the date when their account was created, making use of the expressions added above. + +The `tolerance` option allows you to make the discount usable for a longer period of time (for example, a day before or after the registration date) to allow more time for the customers to use it. + +Create the condition by creating a class implementing the [`DiscountConditionInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountConditionInterface.html): + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Condition/IsAccountAnniversary.php') =]] +``` + +The `tolerance` option is made available for usage in the expression by passing it in the constructor. +The expression can evaluate to `true` or `false` depending on the custom expressions values. + + + +For each custom condition class, you must create a dedicated condition factory, a class implementing the `\Ibexa\Discounts\Repository\DiscountCondition\DiscountConditionFactoryInterface` inteface. + +This allows you to create conditions when working in the context of the Symfony service container. + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Condition/IsAccountAnniversaryConditionFactory.php') =]] +``` + +Mark it as a service using the `ibexa.discounts.condition.factory` service tag and specify the condition's identifier. + +``` yaml + App\Discounts\Condition\IsAccountAnniversaryConditionFactory: + tags: + - name: ibexa.discounts.condition.factory + discriminator: !php/const App\Discounts\Condition\IsAccountAnniversary::IDENTIFIER +``` + +You can now use the condition using the PHP API. + +To learn how to integrate it into the back office, see [Extend Discounts wizard](extend_discounts_wizard.md). + +### Implement custom rules + +The following example implements a [purchasing power parity](https://en.wikipedia.org/wiki/Purchasing_power_parity) discount, adjusting product's price in the cart based on buyer's region. +You could use it, for example, in regions sharing the same currency and apply the rule only to them by using the [`IsInRegions` condition](discounts_api.md#conditions). + +To implement a custom rule, create a class implementing the [`DiscountRuleInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountRuleInterface.html). + + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRule.php', 0, 42) =]] +``` + +As with conditions, create a dedicated rule factory: + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Rule/PurchasingPowerParityRuleFactory.php', 0, 14) =]] +``` + +Then, mark it as a service using the `ibexa.discounts.rule.factory` service tag and specify the rule's type. + +``` yaml + App\Discounts\Rule\PurchasingPowerParityRuleFactory: + tags: + - name: ibexa.discounts.rule.factory + discriminator: !php/const App\Discounts\Rule\PurchasingPowerParityRule::TYPE +``` + +You can now use the rule with the PHP API, but to use it within the back office and storefront you need to: + +- [integrate it into the Discounts wizard](extend_discounts_wizard.md) +- implement a new value formatter + +### Custom discount value formatting + +You can adjust how each discount type is displayed when using the [`ibexa_discounts_render_discount_badge` Twig function](discounts_twig_functions.md#ibexa_discounts_render_discount_badge) by implementing a custom formatter. + +You must implement a custom formatter for each custom rule. + +To do it, create a class implementing the [`DiscountValueFormatterInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountValueFormatterInterface.html) and use the `ibexa.discounts.value.formatter` service tag: + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Rule/PurchaseParityValueFormatter.php') =]] +``` + +``` yaml + App\Discounts\Rule\PurchaseParityValueFormatter: + tags: + - name: ibexa.discounts.value.formatter + rule_type: !php/const App\Discounts\Rule\PurchasingPowerParityRule::TYPE +``` + +## Change discount priority + +You can change the [the defualt discount priority](discounts_guide.md#discounts-priority) by creating a class implementing the [`DiscountPrioritizationStrategyInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountPrioritizationStrategyInterface.html) and aliasing to it the default implementation. + +The example below decorates the default implementation to prioritize recently updated discounts above all the others. +It uses one of the existing [discount search criterions](discounts_criteria.md). + +``` php +[[= include_file('code_samples/discounts/src/Discounts/RecentDiscountPrioritizationStrategy.php') =]] +``` + +``` yaml + App\Discounts\RecentDiscountPrioritizationStrategy: + decorates: Ibexa\Contracts\Discounts\DiscountPrioritizationStrategyInterface + arguments: + $inner: '@.inner' +``` diff --git a/docs/discounts/extend_discounts_wizard.md b/docs/discounts/extend_discounts_wizard.md new file mode 100644 index 0000000000..4df92952ff --- /dev/null +++ b/docs/discounts/extend_discounts_wizard.md @@ -0,0 +1,165 @@ +--- +description: Integrate custom rules and conditions into the back office forms. +editions: + - lts-update + - commerce +month_change: true +--- + +# Extend Discounts wizard + +## Introduction + +For the store managers to use your [custom conditions and rules](extend_discounts.md#implement-custom-condition), you need to integrate them into the back office discounts creation form. + +This form is built using [Symfony Forms]([[= symfony_doc=]]/forms.html) and the [`DiscountFormMapperInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html) interface is at the core of the implementation. + +It provides a two-way mapping between the form structures (used to render the form) and the PHP API values used to create the discounts by offering methods related to: + +- form rendering +- data structure mapping + +Form rendering methods return objects implementing the [`DiscountDataInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Admin-Form-Data-DiscountDataInterface.html), allowing you to access and modify the form data. +They include: + +- [`createFormData()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html#method_createFormData) renders the form before the discount is created +- [`mapDiscountToFormData()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html#method_mapDiscountToFormData) renders the form when the discount already exists. It fills the discount edit form with the saved discount details + +The data mapping methods are responsible for transforming the form data into structures compatible with the [Discount's PHP API](discounts_api.md) services like [`DiscountServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html) and [`DiscountCodeServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-DiscountsCodes-DiscountCodeServiceInterface.html). +They include: + +- [`mapCreateDataToStruct()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html#method_mapCreateDataToStruct) creates the [`DiscountCreateStruct`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Struct-DiscountCreateStruct.html) object to create the discount +- [`mapUpdateDataToStruct()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html#method_mapUpdateDataToStruct) creates the [`DiscountUpdateStruct`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Struct-DiscountUpdateStruct.html) object to update the discount +- [`mapEditTranslateDataToStruct()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html#method_mapEditTranslateDataToStruct) creates the [`TranslationStruct`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Struct-DiscountTranslationStruct.html) objects for [translating the discounts](discounts_api.md#discount-translations) + +In addition to these methods, the main form mapper and the form mappers responsible for each step in the wizard dispatch events that you can use to add your custom logic. +See [discount's form events](discounts_events.md#form-events) for a list of the available events. + +## Integrate custom conditions + +This example continues the [anniversary discount condition example](extend_discounts.md#implement-custom-condition), integrating the condition with the wizard by adding a dedicated step with condition options. +The example limits the new step to cart discounts only. + +To add a custom step, create a value object representing the step. +It contains the step identifier, properties for storing form data, and extends the [`AbstractDiscountStep`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Admin-Form-Data-AbstractDiscountStep.html): + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Step/AnniversaryConditionStep.php') =]] +``` + +Then, create a new event listener listening to the [`CreateFormDataEvent` and `MapDiscountToFormDataEvent` events](discounts_events.md#form): + +``` php hl_lines="18-19 26-50" +[[= include_file('code_samples/discounts/src/Discounts/Step/Step1/AnniversaryConditionStepEventSubscriber.php') =]] +``` + +Attaching the `addAnniversaryConditionStep()` method to both these events adds the custom step both in discount creation and edit forms. + +The method first verifies if the form renders the cart discount wizard, according to assumptions of this example. + +Then, it creates the `AnniversaryConditionStep` object. +If the discount existed already and is being edited, the saved values are used to populate the form. + +Finally, the new step is added to the wizard using the `withStep()` method, using `45` as step priority. +Each of the existing form steps has its own priority, allowing you to add your custom steps between them. + +| Step name | Priority | +|---| ---| +| General properties | 50| +| Target group | -20 | +| Products | -30 | +| Conditions | -40 | +| Discount value | -50 | +| Summary | -1000 | + +The custom step is added between the "Conditions" and "Discount value" steps. + +To add form fields to it, create an event listener adding your fields and a custom form type: + +``` php +[[= include_file('code_samples/discounts/src/Discounts/Step/AnniversaryConditionStepFormListener.php') =]] +``` + +``` php +[[= include_file('code_samples/discounts/src/Form/Type/AnniversaryConditionStepType.php') =]] +``` + +The new form step, including its form fields, are now part of the discounts wizard. + +The last task is making sure that the form data is correctly saved by attaching it to the discounts API structs. + +Expand the previously created `AnniversaryConditionStepEventSubscriber` to listen to two additional events: + +- [`CreateDiscountCreateStructEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Event-CreateDiscountCreateStructEvent.html) +- [`CreateDiscountUpdateStructEvent`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Event-CreateDiscountUpdateStructEvent.html) + +and add the `addStepDataToStruct()` method: + +``` php hl_lines="23-24 57-70" +[[= include_file('code_samples/discounts/src/Discounts/Step/Step2/AnniversaryConditionStepEventSubscriber.php') =]] +``` + +When the form is submitted, this method extracts information whether the store manager enabled the anniversary discount in the form and adds the condition to make sure this data is properly saved. + +The custom condition is now integrated with the discounts wizard and can be used by store managers to attract new customers. + +## Integrate custom rules + +This example continues the [purchasing power parity rule example](extend_discounts.md#implement-custom-rules), integrating the rule with the wizard. + +First, create a new service implementing the `DiscountValueMapperInterface` interface, responsible for handling the new rule type: + +``` php hl_lines="59-60" +[[= include_file('code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityValueMapper.php') =]] +``` + +It uses an `PurchasingPowerParityValue` object to store the form data: + +``` php +[[= include_file('code_samples/discounts/src/Form/Data/PurchasingPowerParityValue.php') =]] +``` + +This value mapper is used by a new form mapper, dedicated to the new rule type: + +``` php +[[= include_file('code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityFormMapper.php') =]] +``` + +Link them together when defining the services: + +``` yaml + App\Form\FormMapper\PurchasingPowerParityValueMapper: ~ + + App\Form\FormMapper\PurchasingPowerParityFormMapper: + arguments: + $discountValueMapper: '@App\Form\FormMapper\PurchasingPowerParityValueMapper' +``` + +The [`DiscountFormMapperInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html) acts as a registry, finding a form mapper dedicated for given rule type and delegating to the responsibility of building the form. + +As each rule type might have a different rule calculation logic, each rule must have a different "Discount value" step in the form. + +To create it, create a dedicated class implementing the [`DiscountValueFormTypeMapperInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Admin-Form-DiscountValueFormTypeMapperInterface.html) + +``` php +[[= include_file('code_samples/discounts/src/Form/FormMapper/PurchasingPowerParityDiscountValueFormTypeMapper.php') =]] +``` + +and add a dedicated value type class: + +``` php hl_lines="26-40 47-61 72" +[[= include_file('code_samples/discounts/src/Form/Type/DiscountValue/PurchasingPowerParityValueType.php') =]] +``` + +In the example above, the discount value step is used to display a read-only field with regions the discount is limited to. +The `$availableRegionHandler` callback function extracts the selected regions and modifies the form as needed, using the `FormEvents::PRE_SET_DATA` and `FormEvents::POST_SUBMIT` events. + +The last step consists of providing all the required translations. +Specify them in `translations/ibexa_discount.en.yaml`: + +``` yaml +ibexa.discount.type.purchasing_power_parity: Purchasing Power Parity +discount.rule_type.purchasing_power_parity: Purchasing Power Parity +``` + +The custom rule is now integrated with the discounts wizard and can be used by store managers to offer new discounts. diff --git a/docs/permissions/policies.md b/docs/permissions/policies.md index b651676f44..3764b0905d 100644 --- a/docs/permissions/policies.md +++ b/docs/permissions/policies.md @@ -127,7 +127,7 @@ Each role you assign to user or user group consists of policies which define, wh #### Discounts [[% include 'snippets/lts-update_badge.md' %]] [[% include 'snippets/commerce_badge.md' %]] -The discount policies decide which actions can be executed by given user or user group. +The [discount](discounts.md) policies decide which actions can be executed by given user or user group. !!! caution "Customers and discount policies" diff --git a/docs/pim/price_api.md b/docs/pim/price_api.md index 9630453638..58681bafff 100644 --- a/docs/pim/price_api.md +++ b/docs/pim/price_api.md @@ -6,7 +6,7 @@ description: Use PHP API to manage currencies in the shop and product prices. ## Currencies -To manage currencies, use `CurrencyServiceInterface`. +To manage currencies, use [`CurrencyServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-CurrencyServiceInterface.html). To access a currency object by its code, use `CurrencyServiceInterface::getCurrencyByCode`. To access a whole list of currencies, use `CurrencyServiceInterface::findCurrencies`. @@ -23,7 +23,7 @@ To create a new currency, use `CurrencyServiceInterface::createCurrency()` and p ## Prices -To manage prices, use `ProductPriceService`. +To manage prices, use [`ProductPriceServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductPriceServiceInterface.html). To retrieve the price of a product in the currency for the current context, use `Product::getPrice()`: @@ -37,7 +37,7 @@ To retrieve the price of a product in a specific currency, use `ProductPriceServ [[= include_file('code_samples/api/product_catalog/src/Command/ProductPriceCommand.php', 79, 82) =]] ``` -To get all prices (in different currencies) for a given product, use `ProductPriceService::findPricesByProductCode`: +To get all prices (in different currencies) for a given product, use `ProductPriceServiceInterface::findPricesByProductCode`: ``` php [[= include_file('code_samples/api/product_catalog/src/Command/ProductPriceCommand.php', 93, 99) =]] @@ -51,7 +51,7 @@ To load price definitions that match given criteria, use `ProductPriceServiceInt [[= include_file('code_samples/api/product_catalog/src/Command/ProductPriceCommand.php', 100, 110) =]] ``` -You can also use `ProductPriceService` to create or modify existing prices. +You can also use `ProductPriceServiceInterface` to create or modify existing prices. For example, to create a new price for a given currency, use `ProductPriceService::createProductPrice` and provide it with a `ProductPriceCreateStruct` object: ``` php @@ -68,12 +68,13 @@ For example, to create a new price for a given currency, use `ProductPriceServic To display a product price on a product page or in the cart, you must calculate its value based on a base price and the context. Context contains information about any price modifiers that may apply to a specific customer group. -To determine the final price, or resolve the price, use the `PriceResolverInterface` service, which uses the following logic: +To determine the final price, or resolve the price, use the [`PriceResolverInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-PriceResolverInterface.html) service, which takes the following conditions into account: -1. Checks whether a price exists for the product and currency, returns `null` if no such price exists. -2. Verifies whether a customer group-related modifier exists: - 1. If yes, it returns a custom price that is valid for the selected customer group. - 2. If not, it returns a base product price in the selected currency. +1. Existance of base price for the product in the specified currency +2. Existance of customer group-related modifiers +3. Existance of applicable [discounts](discounts.md) + +If the base price in the specified currency is missing, the return value is `null`. To resolve a price of a product in the currency for the current context, use either `PriceResolverInterface::resolvePrice()` or `PriceResolverInterface::resolvePrices()`: @@ -85,8 +86,8 @@ To resolve a price of a product in the currency for the current context, use eit ## VAT -To get information about the VAT categories and rates configured in the system, use `VatServiceInterface`. -VAT is configured per region, so you also need to use `RegionServiceInterface` to get the relevant region object. +To get information about the VAT categories and rates configured in the system, use [`VatServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-VatServiceInterface.html). +VAT is configured per region, so you also need to use [`RegionServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-RegionServiceInterface.html) to get the relevant region object. ``` php [[= include_file('code_samples/api/product_catalog/src/Command/VatCommand.php', 49, 50) =]] diff --git a/docs/search/discounts_search_reference/discounts_criteria.md b/docs/search/discounts_search_reference/discounts_criteria.md index 87631d8597..4a6ea3bace 100644 --- a/docs/search/discounts_search_reference/discounts_criteria.md +++ b/docs/search/discounts_search_reference/discounts_criteria.md @@ -3,11 +3,12 @@ month_change: false editions: - lts-update - commerce +description: Search Criteria available for Discounts search --- # Discounts Search Criterion reference -Search Criteria are found in the `Ibexa\Contracts\Discounts\Value\Query\Criterion` namespace, implementing the [CriterionInterface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Query-CriterionInterface.html) interface: +Search Criteria are found in the [`Ibexa\Contracts\Discounts\Value\Query\Criterion`](/api/php_api/php_api_reference/namespaces/ibexa-contracts-discounts-value-query-criterion.html) namespace, implementing the [CriterionInterface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Query-CriterionInterface.html) interface: | Criterion | Description | |---|---| diff --git a/docs/search/discounts_search_reference/discounts_sort_clauses.md b/docs/search/discounts_search_reference/discounts_sort_clauses.md index 148f8e5df8..585cdead1d 100644 --- a/docs/search/discounts_search_reference/discounts_sort_clauses.md +++ b/docs/search/discounts_search_reference/discounts_sort_clauses.md @@ -3,6 +3,7 @@ month_change: false editions: - lts-update - commerce +description: Sort Clauses available for Discounts search --- # Discounts Search Sort Clauses reference diff --git a/mkdocs.yml b/mkdocs.yml index 7cb7311839..6e21cd16b1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -416,6 +416,8 @@ nav: - Install Discounts: discounts/install_discounts.md - Customize Discounts: discounts/configure_discounts.md - Discounts API: discounts/discounts_api.md + - Extend Discounts: discounts/extend_discounts.md + - Extend Discounts wizard: discounts/extend_discounts_wizard.md - Customer management: - Customer Portal: customer_management/customer_portal.md - Customer Portal guide: customer_management/customer_portal_guide.md