lobbster / module-discount-filter
Indexed is_discounted flag for layered navigation and Elastic/OpenSearch facets
Package info
github.com/lobbster-doo/discount-filter
Type:magento2-module
pkg:composer/lobbster/module-discount-filter
Requires
- php: >=8.2
- magento/framework: *
- magento/module-catalog: *
- magento/module-catalog-inventory: *
- magento/module-catalog-rule: *
- magento/module-catalog-search: *
- magento/module-configurable-product: *
- magento/module-downloadable: *
- magento/module-eav: *
- magento/module-inventory-indexer: *
- magento/module-inventory-sales-api: *
- magento/module-store: *
README
Adds an indexed boolean product attribute — is_discounted — derived from catalog_product_index_price (final_price < price, tier prices included). The module powers a Sale-style layered-navigation facet and an Elastic/OpenSearch-ready attribute without relying on ad-hoc SQL at render time.
A staging table, lobbster_discountfilter_flag, stores the authoritative per website × customer group truth. The EAV attribute is a website-level roll-up of that staging data, scoped for layered navigation and search facets.
Installation
Install via Composer:
composer require lobbster/module-discount-filter
Enable the module, run setup, and reindex the affected indexers:
bin/magento module:enable Lobbster_DiscountFilter bin/magento setup:upgrade bin/magento indexer:reindex catalog_product_price lobbster_discountfilter catalog_product_flat catalogsearch_fulltext
Commands
Full rebuild — staging + EAV rollup + downstream flat/fulltext chunking:
bin/magento lobbster:discountfilter:rebuild
Kill switch — truncate staging and zero is_discounted for every catalog_product_website row:
bin/magento lobbster:discountfilter:reset
Attribute
| Property | Value |
|---|---|
| Code | is_discounted |
| Scope | Website |
| Layered navigation | Yes — Sale-style facet. Only the “Yes” option renders; if nothing in the result set is discounted the whole filter block is hidden. The “Yes” label is configurable (see Configuration). |
Indexers
| ID | Role |
|---|---|
lobbster_discountfilter |
Rebuilds staging + EAV roll-up. Depends on catalog_product_price and catalogrule_product. |
catalog_product_flat |
Declared to depend on lobbster_discountfilter, so flat tables rebuild after is_discounted changes. |
Materialized-view subscriptions (etc/mview.xml): catalog_product_index_price, catalog_product_super_link, catalog_product_website, cataloginventory_stock_status, catalogrule_product_price, customer_group.
Configuration
Stores → Configuration → Lobbster → Discount Filter
| Field | Default | Notes |
|---|---|---|
| Enable Indexer | Yes | When disabled, indexer execute* methods no-op. Use lobbster:discountfilter:reset to zero existing values. |
| EAV Roll-up Strategy | any_group |
any_group marks the attribute true if any customer group is discounted on the website. not_logged_in_only only considers guest / group 0. |
| Discount facet option label | Yes |
Yes matches the native Sale filter style; On Sale gives explicit wording. Store-scope supported. |
| Indexer Batch Size | 5000 |
Products per batch during a full rebuild. |
| Downstream Reindex Chunk Size | — | Chunk size for catalog_product_flat / catalogsearch_fulltext reindexList calls. |
Facet block title vs option label
Layered navigation shows two different labels, edited in different places:
| What you see | Where to change it |
|---|---|
| Facet block heading (e.g. “Is Discounted”) | Admin → Stores → Attributes → Product → is_discounted → Labels (default + store view labels). This module does not hardcode the group title. Flush Blocks HTML / Full Page cache after changes. |
| Single facet option (“Yes” vs “On Sale”) | Stores → Configuration → Lobbster → Discount Filter → Layered navigation → Discount facet option label. Kept separate from the attribute’s boolean option source so the storefront can match Sale-style wording without touching EAV options. |
API for non-search code paths
The EAV roll-up is intentionally lossy at the customer-group level. For accurate group-scoped checks (cart rules, promo blocks, bespoke reports), inject the staging-backed interface:
use Lobbster\DiscountFilter\Api\DiscountFlagLookupInterface; public function __construct(private readonly DiscountFlagLookupInterface $lookup) {} $this->lookup->isDiscounted($productId, $websiteId, $customerGroupId); $this->lookup->filterDiscounted($productIds, $websiteId, $customerGroupId);
Supported product types
The indexer computes is_discounted for simple, virtual, downloadable, and configurable products. Configurable parents only consider saleable children (inventory_stock_* when MSI is enabled, otherwise cataloginventory_stock_status). Bundle and grouped products remain 0 until phase-2 logic is added.
Operational notes
- Keep
catalog_product_priceup to date;is_discountedis derived from itsfinal_price/price. - Tier pricing counts as discounted when it lowers
final_pricebelowprice. - With flat catalog product enabled, the pipeline triggers
catalog_product_flatfor affected IDs after flag changes. - Elastic/OpenSearch facets can’t natively facet
is_discountedper customer group — use the staging-backed API above for group-accurate logic.
Testing
./vendor/bin/phpunit --configuration dev/tests/unit/phpunit.xml.dist app/code/Lobbster/DiscountFilter/Test/Unit
Coding standard
Run from the Magento project root:
vendor/bin/phpcs --standard=app/code/Lobbster/DiscountFilter/phpcs.xml app/code/Lobbster/DiscountFilter
# or:
composer cs:discountfilter
The module ships phpcs.xml referencing Magento2 + Magento2Framework from magento/magento-coding-standard. Adobe copyright sniffs are disabled for this path (see rule severity="0" in the module phpcs.xml and the matching exclude-pattern in the project-root phpcs.xml) so Lobbster file headers aren’t compared to Adobe’s copyright. All other rules — including PHPDoc sniffs — apply in full.
License
Use, copy, modify, and distribute this software free of charge for non-commercial purposes only. Commercial use — including selling, sublicensing for a fee, or using the software primarily to operate a business for profit — is not permitted without separate written permission from the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Canonical text: LICENSE.