By default catalogrule promotions are applied to price field. Changing this was one classic request in most Magento 1 projects we have managed

One approach could be to apply catalogrule promotions to the lowest value of price & special_price, for instance, this way https://stackoverflow.com/questions/18120342/catalog-price-rules-applied-to-special-price

A better, more complete, approach could be having a price selector when editing catalogrule rules, so admin users can choose which price they want to apply for the rule they are editing

In Magento2, I have seen price value seems to be retrieved in Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder::build() method

$priceAttr = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'price');

This is called, among other places, in Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::execute() method, which seems to be responsible for catalog rule prices updates

So, the easy part (using special_price instead of price) apparently could be achieved just rewriting that Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder::build() method

Any ideas to achieve the hard one?

  • Maybe I am missing the question. But in Magento 2, if the Special Price is lower than the Catalog Price rule, then the lower Special Price is shown and the Catalog Price rule is ignored. It would seem to me, the simpler way would be to factor for the Catalog Price rule knowing it only affects the price IF the resulting rule would result in a price lower than the Special Price.
    – dawhoo
    Commented Nov 21, 2018 at 20:13
  • Maybe I was not clear... The point is, assuming catalog products have both special & special_price, we need to create (for example) one catalogrule which makes 20% from price, and another which represents 20% from special_price. Having catalogrules based in special_price is a common feature, here in Spain at least. It is a typical promotion of one day or a short period of time Commented Nov 22, 2018 at 6:28

2 Answers 2


I post our solution, after some working days

This module install a new attribute for catalog rules (price_source, which is a selector of price / special_price)

Then, we rewrite the needed classes to add special_price values when calculating the price when applying the catalog rules


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <preference for="Magento\CatalogRule\Model\Indexer\ProductPriceCalculator" type="Sinapsis\CatalogRule\Model\Indexer\ProductPriceCalculator" />
    <preference for="Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder" type="Sinapsis\CatalogRule\Model\Indexer\RuleProductsSelectBuilder" />


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="module.xsd">
    <module name="Sinapsis_CatalogRule" setup_version="1.0.0">


public function calculate($ruleData, $productData = null)
    if ($productData !== null && isset($productData['rule_price'])) {
        $productPrice = $productData['rule_price'];
    } else {
        // get price source from rule
        $productPrice = $ruleData['default_price'];
        if ($ruleData['price_source'] == 2) {
            $productPrice = $ruleData['default_special_price'];


public function build(
    \Magento\Catalog\Model\Product $product = null,
    $useAdditionalTable = false
) {

    if ($product && $product->getEntityId()) {
        $select->where('rp.product_id=?', $product->getEntityId());

    // join price_source
        ['catalogrule' => $this->resource->getTableName('catalogrule')],

    $linkField = $this->metadataPool
        ['e' => $this->resource->getTableName('catalog_product_entity')],
        sprintf('e.entity_id = rp.product_id'),

     * Join default price and websites prices to result

    foreach (array('price', 'special_price') as $sourcePrice) {
        $priceTableAlias = 'pp_default';
        if ($sourcePrice == 'special_price') {
            $priceTableAlias = 'sp_default';

        $priceAttr = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $sourcePrice);
        $priceTable = $priceAttr->getBackend()->getTable();
        $attributeId = $priceAttr->getId();

        $joinCondition = '%1$s.' . $linkField . '=e.' . $linkField . ' AND (%1$s.attribute_id='
            . $attributeId
            . ') and %1$s.store_id=%2$s';

            [$priceTableAlias => $priceTable],
            sprintf($joinCondition, $priceTableAlias, \Magento\Store\Model\Store::DEFAULT_STORE_ID),

    $website = $this->storeManager->getWebsite($websiteId);
    $defaultGroup = $website->getDefaultGroup();
    if ($defaultGroup instanceof \Magento\Store\Model\Group) {
        $storeId = $defaultGroup->getDefaultStoreId();
    } else {
        $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID;

        ['product_website' => $this->resource->getTableName('catalog_product_website')],
        'product_website.product_id=rp.product_id '
        . 'AND product_website.website_id = rp.website_id '
        . 'AND product_website.website_id='
        . $websiteId,

    foreach (array('price', 'special_price') as $sourcePrice) {
        $priceKey = 'pp';
        if ($sourcePrice == 'special_price') {
            $priceKey = 'sp';
        $priceTableAlias = $priceKey . '_default';

        $priceAttr = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $sourcePrice);
        $priceTable = $priceAttr->getBackend()->getTable();

        $tableAlias = $priceKey . $websiteId;
            [$tableAlias => $priceTable],
            sprintf($joinCondition, $tableAlias, $storeId),
            'default_' . $sourcePrice => $connection->getIfNullSql($tableAlias . '.value', $priceTableAlias .'.value'),
    return $connection->query($select);


namespace Sinapsis\CatalogRule\Model;

use Magento\Framework\Data\OptionSourceInterface;

class PriceOptions implements OptionSourceInterface

    public function toOptionArray()
        return [
            ['label' => __('Price'), 'value' => '1'],
            ['label' => __('Special price'), 'value' => '2']


namespace Sinapsis\CatalogRule\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface

    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
        $installer = $setup;

                'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
                'length' => 1,
                'nullable' => false,
                'default' => 1,
                'comment' => 'Price Source'



<?xml version="1.0" encoding="UTF-8"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <fieldset name="rule_information">
        <field name="price_source" formElement="select">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="source" xsi:type="string">catalog_rule</item>
                <label translate="true">Price Source</label>
                    <rule name="required-entry" xsi:type="boolean">true</rule>
                        <options class="Sinapsis\CatalogRule\Model\PriceOptions"/>



on magento 2.4.2 when reindex with your module shows Class Sinapsis\CatalogRule\Model\Indexer\ProductPriceCalculator does not exist

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.