All posts
Forward Development

Why Magento 2 catalog price rules can't target a single store view

Magento 2 catalog price rules stop at website and customer group scope. Here is why, and what each workaround costs in indexing, caching and upkeep.

  • Magento 2
  • Adobe Commerce
  • Operations

You run English, French and German store views under one website. Marketing wants a 15% catalog discount in the French store view only. You open Marketing > Catalog Price Rule, and the rule form gives you Websites and Customer Groups. There is no store view selector. That is not a missing feature someone forgot to ship. It follows from how Magento 2 scopes price, and working around it touches more of the system than the rule form suggests.

A common first attempt is to add a store_id column to catalogrule, catalogrule_product and catalogrule_product_price, then override the rule resource model with a preference so getRulePrices() filters on the current store. That approach works for simple products on Magento 2.4.5 and fails for configurables. The reason it fails tells you most of what you need to know about the problem.

Price scope stops at the website

Under Stores > Configuration > Catalog > Catalog > Price, Catalog Price Scope has two options: Global or Website. Store view is not one of them. A product's price attribute can differ per website and no further. The base currency also lives at website level. A store view can display a different currency, but that is a display conversion and does not give the store view its own price.

Catalog price rules follow the same model. A rule is assigned to one or more websites and one or more customer groups. When the rule indexer runs, it writes resolved prices into catalogrule_product_price keyed by rule_date, website_id, customer_group_id and product_id. Store view does not appear in that key because nothing upstream of it has a store-view price either.

This matters because a store view in Magento is mostly a presentation layer: language, labels, URL keys, content. Commerce state (price, base currency, cart, and in most setups stock) belongs to the website. Store-view pricing would put a commerce decision into the presentation layer, and Magento's data model has nowhere to keep it.

The rule index feeds the price index

Catalog rules are not evaluated live on each request. Three indexers are involved: catalogrule_rule and catalogrule_product resolve which products match which rules, and the product price indexer (catalog_product_price) folds rule prices into catalog_product_index_price. That price index is what category listings, layered navigation price filters, sorting by price and configurable price ranges read from.

The price index has its own dimensions, which you can see with bin/magento indexer:set-dimensions-mode catalog_product_price. The available modes are none, website, customer_group, and website_and_customer_group. Store view is not an option there either.

This explains the configurable failure. A simple product's final price on its own page can be resolved at render time through the catalog rule price model, which calls the resource model you overrode, with the store manager returning the frontend store. A configurable's displayed price and the prices of its children in listings come from the price index, which was built by an indexer process. In that process there is no frontend store context, and nothing in the index has a store column to filter on. You patched the read path for one rendering route. The data in the index was never store-specific.

Patching the indexer as well means adding a store dimension to catalog_product_index_price and every table and query that touches it, including the configurable and bundle price modifiers. Each one is a core override you then carry through every 2.4.x upgrade.

Splitting into separate websites

If prices really differ by market, the structurally correct answer is to make each market its own website. Then the price attribute, base currency, catalog rules and price index all line up without custom code.

The costs are real but they are well understood:

  • Price index rows multiply by websites times customer groups. On a large catalog with many groups, reindex time and table size grow noticeably. Website dimension mode keeps the tables partitioned.
  • Carts are per website. A customer switching from FR to DE loses the cart unless you handle it.
  • Customer account sharing defaults to per website. Set it to global if one login should work everywhere.
  • Product website assignments, website-scope attribute values and inventory source mapping all need to be maintained per website.
  • Full page cache already varies by store, so nothing new goes wrong there.

We would choose this in most cases. It is more configuration, but it is configuration Magento expects, and it survives upgrades.

Using customer groups or shared catalogs

Customer groups are the other native rule dimension, so a common idea is to map a store view to a customer group. The problem is that a group belongs to the customer, not the store. Guests are always NOT LOGGED IN. To make this work you would need a plugin that overrides the session's group ID based on the current store view. That breaks for logged-in B2B customers who need their real group, it affects tax class resolution, and it makes any group-based cart rule unpredictable.

Shared catalogs on Adobe Commerce B2B are built on customer groups and have the same limit. They are the right tool for company-specific pricing and the wrong tool for language- or region-specific pricing under one website.

Use this route only if the store views already map cleanly onto distinct audiences who log in, such as a trade store view and a retail store view. Even then, separate websites are usually cleaner.

Writing a custom price modifier

If the business can't split websites, for example because of a shared cart or shared stock, a custom implementation is possible. It has to be complete or it will produce wrong prices:

  • Store rule assignments in your own table rather than altering core tables, so upgrades don't conflict with core schema.
  • Add a price index modifier so listings, filters, sorting and configurable or bundle ranges reflect the store-view price, which means adding a store dimension to the index or keeping a parallel store-scoped index.
  • Hook the final price used by the quote, so the cart total matches the product page. If you only patch rendering, the cart charges the website price while the page shows the store-view price.
  • Make sure every indexer runs per store explicitly instead of relying on the store manager, which returns the admin store during indexing and cron.
  • Check GraphQL and REST price resolution if anything headless reads from the store.

Each of these is a point where a minor release can change a signature or a query. Budget for regression testing on every upgrade, and for reindex time that grows with the number of store views.

What to do next

Start by deciding whether the store views differ in price or only in language. If only the language differs, keep one website and drop the requirement. If the price differs, plan the split into separate websites, and test cart, account and inventory behaviour on staging before moving catalog data. Only build a custom store-scoped price layer if a hard constraint rules out separate websites, and then scope it across the index, quote and API rather than the resource model alone.

If you are weighing these options on a live store, our Magento 2 practice covers this kind of scope and indexing work. Get in touch with your store and website structure and we can tell you which route fits.

Need this done on a real stack?

Magento 2, Adobe Commerce, migrations to Medusa.js or Vendure, enterprise Next.js, WordPress, and AI automation.

Contact us