Include tier prices in catalog rule indexer to prevent rule from raising effective price - #272
Open
ddevallan wants to merge 3 commits into
Open
Include tier prices in catalog rule indexer to prevent rule from raising effective price#272ddevallan wants to merge 3 commits into
ddevallan wants to merge 3 commits into
Conversation
…ing effective price The catalog rule indexer built default_price from the regular product price only. When a catalog rule calculated a discounted price higher than an existing tier price, the indexed rule price was used instead of the tier price, effectively raising the price for customers who qualified for both. Added LEFT JOINs for website-scoped and global tier prices at qty=1, using OR all_groups=1 to correctly handle tier prices that apply to all customer groups. The default_price column now uses LEAST() so the catalog rule discount is applied against the lowest available price, preventing the rule from ever producing a price higher than an existing tier price.
ddevallan
marked this pull request as draft
June 1, 2026 19:44
Two integration tests against the full IndexerBuilder pipeline verify the tier price fix across all customer groups: 1. testReindexFullRulePriceNeverExceedsTierPrice: with a all-groups global tier and 50% rule (which would give ), the indexed rule price must be ≤ for all customer groups. 2. testReindexFullRulePriceWinsWhenLowerThanTierPrice: with a tier and the same 50% rule (which gives ), the rule price wins and must still be ≤ for all groups.
… rule scenarios Extends integration test coverage to the full scenario matrix: 1. All-groups global tier caps rule (already existed) 2. Rule wins when lower than tier (already existed) 3. Specific customer group tier applies only to that group 4. Website-scoped tier price (price_tier JOIN, not global) 5. Both global and website tiers exist — LEAST picks the lower one 6. Volume discount (qty>1): intentionally not considered by indexer — documents the known limitation with an explicit assertion
ddevallan
marked this pull request as ready for review
June 2, 2026 11:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The catalog rule indexer (
RuleProductsSelectBuilder::buildSelect()) computeddefault_pricefrom the regular product price only — tier prices were completely ignored.When a catalog rule calculates a discounted price that is still higher than an existing tier price, the indexed rule price is used instead of the tier price, effectively raising the visible price for customers who qualify for both.
Example: Regular $100 · Tier $30 (all groups) · 50% rule → $50 without fix. With fix: LEAST($30,$100)=$30 → 50%=$15 ≤ $30 ✓
Fix
Added two
LEFT JOINs oncatalog_product_entity_tier_pricefor website-specific and global (website_id=0) tier prices, usingOR all_groups = 1to handle tier prices that apply to all customer groups.default_priceis wrapped inLEAST():LEAST( IFNULL(price_tier0.value, pp_default.value), -- global tier or regular IFNULL(price_tier.value, pp_default.value), -- website tier or regular IFNULL(pp_website.value, pp_default.value) -- regular price )qty = 1 only — intentional design decision
Only tier prices with
qty = 1are joined. This is deliberate: the catalog rule indexer'sdefault_pricerepresents the single-unit display price shown in product listings.Consider a product with regular price $100, a
qty ≥ 5tier price of $20, and a 50% rule. Including the qty=5 tier inLEAST()would give $10 as the indexed rule price — but $10 only applies when buying 5+ units. A customer buying 1 unit would see $10 in listings and pay $50 at checkout. This would be misleading.qty = 1tier prices are semantically different: they apply from the very first unit (effectively group-specific prices) and are safe to include in the single-unit price calculation. The integration testtestReindexFullTierPriceForQtyAboveOneIsNotConsideredByIndexerexplicitly documents this behavior.Other limitations
LEFT JOINs on the indexer SELECT will slightly increase full reindex time on large catalogsall_groups = 1tier AND a specific-group tier for the same website and qty=1, both match the JOIN and may produce duplicate result rowsTest results
Unit tests (3):
Integration tests — full scenario matrix (6):
price_tierJOIN)Existing indexer tests — no regression (3):
Contribution checklist