lobbster/module-discount-filter

Indexed is_discounted flag for layered navigation and Elastic/OpenSearch facets

Maintainers

Package info

github.com/lobbster-doo/discount-filter

Type:magento2-module

pkg:composer/lobbster/module-discount-filter

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-04-22 00:33 UTC

This package is auto-updated.

Last update: 2026-04-22 00:34:57 UTC


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

  1. Keep catalog_product_price up to date; is_discounted is derived from its final_price / price.
  2. Tier pricing counts as discounted when it lowers final_price below price.
  3. With flat catalog product enabled, the pipeline triggers catalog_product_flat for affected IDs after flag changes.
  4. Elastic/OpenSearch facets can’t natively facet is_discounted per 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.