REGISTER NOW
Thursday • 25.08.22 • 17:18
7 minutes for reading
Yevhenii Trishyn
Magento Senior Full-Stack Developer | IT Delight
Back-EndMagentoUncategorized

Pricing types and how pricing works in Magento2

Author: Evgeny Trishin, Senior Magento developer, IT Deligh

Magento2 provides a very advanced and powerful functionality for working with product prices. In the standard edition, the system provides us with the ability to set different types of prices and change the pricing logic depending on the customer group, the number of goods ordered, or promotional campaigns.

This functionality is implemented through different price types for the product. The product has:

  • regular price;
  • price that can be set for a specific customer group;
  • the price of a product as determined by the quantity chosen by a constumer;
  • MSRP – Suggested Retail Price.

Since each product employs its own price model, the logic used to determine those prices varies. This is a class that implements the calculation logic and contains the final result – a certain price value. It must implement the \Magento\Framework\Pricing\Price\PriceIntrface interface.

Due to Magento’s modular architecture, several pricing schemes may be implemented in separate modules. New prices can be incorporated into the system’s overall calculation process by editing the di.xml file. All price models are added to the object\Magento\Catalog\Pricing\Price\Pool. This is a virtual type based on the \Magento\Framework\Pricing\Price\Pool class. A price declaration may be checked through it, and the corresponding pricing model can be accessed. A price ad looks like this:

<virtualType name="Magento\Catalog\Pricing\Price\Pool" type="Magento\Framework\Pricing\Price\Pool">
   <arguments>
       <argument name="prices" xsi:type="array">
           <item name="regular_price" xsi:type="string">Magento\Catalog\Pricing\Price\RegularPrice</item>
           <item name="final_price" xsi:type="string">Magento\Catalog\Pricing\Price\FinalPrice</item>
           <item name="tier_price" xsi:type="string">Magento\Catalog\Pricing\Price\TierPrice</item>
           <item name="special_price" xsi:type="string">Magento\Catalog\Pricing\Price\SpecialPrice</item>
           <item name="base_price"
xsi:type="string">Magento\Catalog\Pricing\Price\BasePrice</item>
           <item name="custom_option_price" xsi:type="string">Magento\Catalog\Pricing\Price\CustomOptionPrice</item>
           <item name="configured_price" xsi:type="string">Magento\Catalog\Pricing\Price\ConfiguredPrice</item>
           <item name="configured_regular_price" xsi:type="string">Magento\Catalog\Pricing\Price\ConfiguredRegularPrice</item>
       </argument>
   </arguments>
</virtualType>

(file vendor/magento/module/catalog/etc/di.xml)

Further, this object is injected through di.xml into the \Magento\Catalog\Pricing\Price\Collection class, through which price models are requested for various types of prices.

<type name="Magento\Catalog\Pricing\Price\Collection">
   <arguments>
       <argument name="pool" xsi:type="object">Magento\Catalog\Pricing\Price\Pool</argument>
   </arguments>
</type>

(file vendor/magento/module/catalog/etc/di.xml)

But this object is not directly used when displaying prices on the site pages. The type-model of the product is used to get the price of the product. The product domain model (\Magento\Catalog\Model\Product) calls the type-model method (for example \Magento\Catalog\Model\Product\Type) getPriceInfo through its getPriceInfo method.

This method takes a product model object as an argument (more precisely, an object that implements the \Magento\Framework\Pricing\SaleableInterface interface, but within Magento, this is the product) and returns an object that implements \Magento\Framework\Pricing\PriceInfoInterface and contains one of the class attributes object \Magento\Catalog\Pricing\Price\Collection.

Through the \Magento\Framework\Pricing\PriceInfoInterface object, you can get its model and, accordingly, the value of this price by the type-id of the price. It is this mechanism that is used in the block displaying the price of the product on its page. By default, this is the \Magento\Catalog\Pricing\Render\PriceBox class. This block uses the renderAmount method, which takes as the first argument exactly the price value obtained from the price model through the getAmount method. And this block receives the Price model from the product model through its getPriceType method.

This mechanism makes pricing in Magento flexible and allows us to add our own price type, which will be discussed below. Also, the current pricing method allows us to shape preexisting price categories without rethinking their underlying pricing algorithms.

This can be done by specifying price-adjustment objects or price-modifiers.

Each price can have a list (list|array) of price adjustment classes. To add your own price-adjustment, you need to add such an object to \Magento\Framework\Pricing\Adjustment\Pool through the di.xml file and add the modifier code to \Magento\Framework\Pricing\Adjustment\Collection as follows:

<type name="Magento\Framework\Pricing\Adjustment\Collection">
   <arguments>
       <argument name="adjustments" xsi:type="array">
           <item name="tax" xsi:type="const">Magento\Tax\Pricing\Adjustment::ADJUSTMENT_CODE</item>
       </argument>
   </arguments>
</type>
<type name="Magento\Framework\Pricing\Adjustment\Pool">
   <arguments>
       <argument name="adjustments" xsi:type="array">
           <item name="tax" xsi:type="array">
               <item name="className" xsi:type="string">Magento\Tax\Pricing\Adjustment</item>
               <item name="sortOrder" xsi:type="string">20</item>
           </item>
       </argument>
   </arguments>
</type>

The modifier object must implement the interface

\Magento\Framework\Pricing\Adjustment\AdjustmentInterface.

This is how, in the standard delivery of Magento, the addition of a tax to the price of a product is implemented. And in the same way, we can add our own modifiers and influence the calculation of standard price types.

It is also possible to modify the price, which is calculated by means of indexers. To do this, you need to add your price indexer modifier. This is also done in the di.xml file. Let’s look at how this is implemented in the Magento\CatalogRule module.

<type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface">
   <arguments>
       <argument name="priceModifiers" xsi:type="array">
           <item name="catalogRulePriceModifier"
                 xsi:type="object">Magento\CatalogRule\Model\Indexer\ProductPriceIndexModifier</item>
       </argument>
   </arguments>
</type>

A modifier object is added to the di.xml file. This is the class object that must implement the interface:

\Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface.

This object will have the modifiPrice method, which will implement the logic unique to your project and modify the standard functioning of the price-indexer. The same approach is implemented in Magento, for example, in modules for bundle products and for configurable products. Thus, it is possible to influence the functioning of indexers without using preferences.

Let’s consider how you can add your own price type if your project needs it. For example, we implement price redefinition for a specific type of customer. Let’s say that we have an eav attribute that stores an array in serialized form, in which the key is the id of the customer, and the value is the predefined price.

To do this, you need to create your own price model. This is the class that implements \Magento\Framework\Pricing\PriceInterface.

/**
* Price type final
*/
const PRICE_CODE = 'customer_price';

/**
* @return float|bool
*/
public function getValue()
{
   if (!$this->session->isLoggedIn()) {
       return false;
   }

   $customerPrice = $this->customerPriceModel->getProductPriceByCustomer(
       $this->product,
       (int)$this->session->getCustomerId()
   );
   if ($customerPrice) {
       return $this->priceCurrency->round((float)$customerPrice);
   }

   return $customerPrice;
}

After that, it needs to be added to the pool of price models so that it can be obtained from the PriceInfo class.

Note that if you plan to use such a cast price for a bundle product, then you need to add it to the pool of Price models for the bundle product as follows.

Now we can display this price on the product page using the standard price-renderer block. Simply specify its code in the layout file and add a template for your price in the catalog_product_prices file.

Here is how to do it:

file catalog_product_view.xml

<referenceContainer name="product.info.stock.sku">
   <block class="Magento\Catalog\Pricing\Render" name="product.price.customer" after="product.info.price">
       <arguments>
           <argument name="price_render" xsi:type="string">product.price.render.default</argument>
           <argument name="price_type_code" xsi:type="string">customer_price</argument>
           <argument name="zone" xsi:type="string">item_view</argument>
       </arguments>
   </block>
</referenceContainer>

file catalog_product_prices.xml

<referenceBlock name="render.product.prices">
   <arguments>
       <argument name="default" xsi:type="array">
           <item name="prices" xsi:type="array">
               <item name="customer_price" xsi:type="array">
                   <item name="render_template"
                         xsi:type="string">Jentry_CustomPrice::product/price/customer_price.phtml</item>
               </item>
           </item>
       </argument>
   </arguments>
</referenceBlock>

If our new price is configured for a logged-in customer, they will see it on the product page.. Thus, we can display the desired price for a specific customer.

In this case, the class must implement the interface for your pricing to be considered in the calculation.

\Magento\Framework\Pricing\Price\BasePriceProviderInterface.

Let’s also add a special discount for products (for registered customers), which will depend on the number of months when the customer is a registered user on our site. To do this, we will use the price adjustment functionality.

Let’s also include an exclusive product discount (for registered customers) that scales with the customer’s duration of membership. To do this, we will use the price adjustment functionality.

In our module, we need to create a class that implements the interface \Magento\Framework\Pricing\Adjustment\AdjustmentInterface. The implementation of this interface involves the implementation of several methods:

  • applyAjustment — a method in which we can change a product price
  • extractAdjustment — a method that returns a product price without our modification
  • getAdjustmentCode — returns a unique code for the given adjustment
  • getSortOrder — returns the position of the given class in the list
  • isExcludedWith — checks if this adjustment can be used with another one, by code
  • isIncludedInBasePrice — shows if the adjustment is included in the standard price
  • isIncludedInDisplayPrice — shows if the adjustment is included in the displayed price

We are most interested in 2 methods of price change implementation — applyAjustment and excludeAdjustoment.

In the applyAjustment method, we will check if the user is logged in and if the product’s use_loyalty flag is set. If so, we will give a $5 discount for each month of their registration on the site.

An example of this class:

/**
* Adjustment code tax
*/
const ADJUSTMENT_CODE = 'loyalty_discount';

private $loyaltyCalculator;

private $sortOrder;

public function __construct(Loyalty $loyaltyCalculator, $sortOrder = null)
{
   $this->loyaltyCalculator = $loyaltyCalculator;
   $this->sortOrder = $sortOrder;
}

public function applyAdjustment($amount, SaleableInterface $saleableItem, $context = [])
{
   if ($saleableItem->getData('use_loyalty')) {
       return $this->loyaltyCalculator->calculatePriceWithDiscount((float)$amount);
   }

   return $amount;
}

public function extractAdjustment($amount, SaleableInterface $saleableItem, $context = []): ?float
{
   if ($saleableItem->getData('use_loyalty')) {
       return $this->loyaltyCalculator->extractDiscountFromPrice((float)$amount);
   }

   return $amount;
}

public function getAdjustmentCode(): string
{
   return self::ADJUSTMENT_CODE;
}

public function getSortOrder()
{
   return $this->sortOrder;
}

public function isExcludedWith($adjustmentCode): bool
{
   return $this->getAdjustmentCode() === $adjustmentCode;
}

public function isIncludedInBasePrice(): bool
{
  return false;
}

public function isIncludedInDisplayPrice(): bool
{
   return true;
}

To include our price-adjustment into the calculation of the product price, it must be added to the pool in the di.xml file, as follows:

<type name="Magento\Framework\Pricing\Adjustment\Pool">
   <arguments>
       <argument name="adjustments" xsi:type="array">
           <item name="loyalty_discount" xsi:type="array">
               <item name="className" xsi:type="string">ITDelight\CustomPrice\Pricing\Adjustment</item>
               <item name="sortOrder" xsi:type="string">120</item>
           </item>
       </argument>
   </arguments>
</type>

Thus, our discount will be applied to products if the user is logged in and has been registered for at least a month. Judging by the implementation in the magento2 core, this functionality should be used specifically for price modification, but not for its redefinition. If you want to completely override pricing, then you need to use the cast price type, as shown earlier.

Now let’s set such a custom price for bundle product options. We need to create products that will be bundle product options, assign them a regular price and our cast customer-price. We will use a dynamic price for a bundled product and it will depend on its options and will be displayed on the product page as a range like this:

From $10 — To $100.

If you do all these preparations and go to the product page, you will see that the range works for the normal option product prices, not our customer-prices. This is due to the fact that the prices for options are taken from the catalog_product_index_price table and in our case, we cannot get the customer-price from this table in any way. Therefore, in this case, I suggest adding an after-plugin to the class Magento\Bundle\Model\ResourceModel\Selection\Collection для метода addPriceFilter.

private $session;

private $customerPriceModel;

private $productRepository;

public function __construct(
   Session $session,
   CustomerPriceModel $customerPriceModel,
   ProductRepositoryInterface $productRepository
) {
   $this->session = $session;
   $this->customerPriceModel = $customerPriceModel;
   $this->productRepository = $productRepository;
}

public function afterAddPriceFilter(Subject $subject, Subject $result): Subject
{
   if (!$this->session->isLoggedIn()) {
       return $result;
   }

   $result->walk(function ($item) {
       $this->addCustomerPrice($item);
   });

   return $result;
}

private function addCustomerPrice(Product $item)
{
   try {
       $product = $this->productRepository->get($item->getSku());
   } catch (NoSuchEntityException $e) {
       $product = $item;
   }

   $customerPrice = $this->customerPriceModel->getProductPriceByCustomer($product, (int)$this->session->getCustomerId());
   if ($customerPrice) {
       $price = min($customerPrice, $item->getPrice());
       $finalPrice = min($customerPrice, $item->getFinalPrice());
       $minimalPrice = min($customerPrice, $item->getMinimalPrice());
       $minPrice = min($customerPrice, $item->getMinPrice());
       $maxPrice = max($customerPrice, $item->getMaxPrice());
       $item->setPrice($price);
       $item->setFinalPrice($finalPrice);
       $item->setMinimalPrice($minimalPrice);
       $item->setMinPrice($minPrice);
       $item->setMaxPrice($maxPrice);
   }
}

Now we will get the customer-price for the options in the range-price and in the customize price section.

The plugin may not be the best solution in terms of performance. But in this case, a plugin is added, since we get the id of the current customer from the session and set the price for it. If you just want to modify the price, then you can use the price index modifier as shown above.

I would also like to clarify that in order for our customer-price to be used in the cart, we need to add an observer to the catalog_product_get_final_price event, where we replace the regular price with our customer-price (an example of such an observer can be seen in the Magento\CatalogRule module). In our example, there is a need for an observer for the same reason why we use the plugin – we get the price depending on the specific customer and the price cannot be calculated in advance.

Knowing these native Magento functionalities, you can customize the calculation of the product price, if necessary. This will work on system upgrades as you are using the standard Magento API and the code that follows Magento 2 standards.

Back

Best authors

Sidovolosyi Bogdan
Dmitry Rybkin
Admin Pro Magento
Alexander Galich and Stanislav Matyavin
Alexander Galich
Yevhenii Trishyn
Abramchenko Anton
Sidovolosyi Bohdan
Shuklin Alexander

Similar topics

  • Advanced JS bundling
  • Backend
  • e-commerce
  • graphics
  • Hyvä
  • Hyvä compatible module
  • Hyvä compatible modules for Magento 2
  • Hyvä Styles
  • indexes
  • Integration Tests
  • Jquery Compat
  • JS
  • JS loading
  • loaded JS files
  • Magento 2
  • Magento 2 indexes
  • Magento 2 theme
  • Magento Functional Testing Framework
  • Magento JS bundling
  • Magento JS merge
  • Magento layouts
  • Magento Templates
  • Magento2
  • MagePack bundling
  • Message Broker
  • MySql
  • optimization
  • PhpStorm
  • Pricing
  • Pricing types
  • product prices
  • proxy classes
  • RabbitMQ
  • Redis
  • Redis \
  • Reduce JS size
  • Tailwind CSS
  • Testing Framework
  • Year in Review
  • Оptimization