PHPackages                             pixelperfectat/magento2-module-discount-exclusion - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. pixelperfectat/magento2-module-discount-exclusion

ActiveMagento2-module[Utility &amp; Helpers](/categories/utility)

pixelperfectat/magento2-module-discount-exclusion
=================================================

Advanced Magento 2 module for preventing additional discounts on already discounted products using a flexible, extensible strategy pattern. Provides out-of-the-box support for excluding products with special prices or catalog price rules from further discounting via shopping cart rules.

0.2.1(1mo ago)386MITPHPPHP ^8.2.0

Since Jan 21Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/pixelperfectat/magento2-module-discount-exclusion)[ Packagist](https://packagist.org/packages/pixelperfectat/magento2-module-discount-exclusion)[ RSS](/packages/pixelperfectat-magento2-module-discount-exclusion/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (12)Versions (4)Used By (0)

PixelPerfect Discount Exclusion
===============================

[](#pixelperfect-discount-exclusion)

Extensible Magento 2 module that prevents applying shopping cart (sales rule) discounts to products already discounted by other mechanisms.

Status: Active development. APIs and behavior may change without backward compatibility.

What it does
------------

[](#what-it-does)

- Blocks additional cart discounts when a product is already discounted (e.g., special price, catalog price rules).
- Leaves other cart items eligible for discounts.
- Provides a pluggable, DI-driven architecture to decide:
    - If exclusion strategies should run (Strategy Eligibility Guards).
    - Which exclusion strategies apply to a product.
- **Per-rule bypass toggle** with max-discount logic: when enabled on a rule, the customer receives `max(existing discount, rule discount)` calculated from the regular price, instead of stacking discounts.
- Displays user-friendly messages explaining why coupons weren't applied or were adjusted for specific products.
- Queues messages in the session and displays them only on the cart page for a cleaner UX.
- Automatically removes coupon from cart when no actual discount was applied.

Architecture Overview
---------------------

[](#architecture-overview)

This module uses an **Around Plugin** on `Magento\SalesRule\Model\Validator::process()` to intercept discount validation before Magento applies cart price rules (coupons/promotions) to quote items.

### The Flow

[](#the-flow)

1. **Module State Check**: Plugin checks if module is enabled for the current store view via admin configuration.
2. **Plugin Interception**: When Magento processes a sales rule for a quote item, `ValidatorPlugin::aroundProcess()` intercepts the call.
3. **Product Extraction**: The plugin identifies the actual product (handling configurable products by examining children).
4. **Bypass Check**: If the rule has `bypass_discount_exclusion` enabled, the bypass flow runs instead of the standard exclusion flow.
5. **Guard Evaluation**: Strategy Eligibility Guards run first to determine if exclusion logic should be considered.
6. **Strategy Evaluation**: If guards allow, Discount Exclusion Strategies check if the product already has a discount.
7. **Decision**:
    - **Standard flow**: If excluded, the plugin returns without calling `$proceed()`, blocking the discount. Otherwise, it calls `$proceed()`.
    - **Bypass flow**: `MaxDiscountCalculator` computes the result. The discount may be adjusted (capped to the difference), blocked (existing is better), or allowed fully (stacking fallback for unsupported rule types).
8. **Result Collection**: Excluded and bypassed items are collected by `ExclusionResultCollector` during processing.
9. **User Feedback**: Messages are queued in the session and displayed on the cart page via `CartPageLoadObserver`. On coupon apply, `CouponPostObserver` displays messages immediately.

### Bypass Flow (Max-Discount Logic)

[](#bypass-flow-max-discount-logic)

When a cart rule has the **Bypass Discount Exclusion** toggle enabled, the plugin applies maximum discount logic instead of blocking the discount entirely:

```
ValidatorPlugin::aroundProcess()
├── Rule has bypass_discount_exclusion?
│   ├── Product NOT already discounted → proceed() (normal discount)
│   └── Product IS already discounted → MaxDiscountCalculator
│       ├── STACKING_FALLBACK (cart_fixed/buy_x_get_y) → proceed()
│       ├── EXISTING_BETTER (existing >= rule) → block + message
│       └── ADJUSTED (rule > existing) → proceed() then cap discount + message
└── No bypass → standard exclusion flow

```

**Example:** Product regular price 100, special price 75 (25% off). Coupon is 30% off.

- **Without bypass**: Coupon blocked entirely (product already discounted).
- **With bypass**: `max(25%, 30%) = 30%` → target price 70 → additional discount of 5 applied → customer pays 70.

Core Components
---------------

[](#core-components)

### 1. Strategy Eligibility Guards

[](#1-strategy-eligibility-guards)

**Purpose**: Guards act as gatekeepers that determine whether discount exclusion strategies should run at all. They provide an early exit mechanism to skip expensive strategy evaluation when it doesn't make sense.

**When to use guards**:

- Filter out special rule types (e.g., free gift promotions that shouldn't be blocked)
- Skip zero-price items (no discount can apply)
- Only apply to coupon-based rules (skip automatic promotions)
- Check customer groups, store views, or date ranges
- Validate product types or item states
- Prevent strategy execution based on rule characteristics

**Interface**:

```
namespace PixelPerfect\DiscountExclusion\Api;

interface StrategyEligibilityGuardInterface
{
    /**
     * Determines if discount exclusion strategies should be evaluated
     *
     * @param ProductInterface|Product $product The product being evaluated
     * @param AbstractItem             $item    The quote item
     * @param Rule                     $rule    The sales rule being applied
     * @return bool True if strategies should run, false to skip all strategies
     */
    public function canProcess(
        ProductInterface|Product $product,
        AbstractItem $item,
        Rule $rule
    ): bool;
}
```

**Built-in Guards**:

1. **CouponOnly**: Only applies exclusion logic to coupon-based rules. Automatic cart rules (no coupon required) proceed normally without exclusion checks.

    ```
    // Returns false for automatic rules (COUPON_TYPE_NO_COUPON)
    // Returns true for specific coupon or auto-generated coupon rules
    return $couponType !== Rule::COUPON_TYPE_NO_COUPON;
    ```
2. **Ampromo**: Skips exclusion for "Amasty Free Gift" rules (those rules provide free items, not additional discounts)

    ```
    // Check if rule is an Ampromo free gift rule
    $simpleAction = $rule->getSimpleAction();
    if ($simpleAction && str_contains($simpleAction, 'ampromo')) {
        return false; // Don't block free gift rules
    }
    return true;
    ```
3. **ZeroPrice**: Skips exclusion logic for products with zero final price (no discount applicable)

    ```
    // Returns false if price is zero, preventing unnecessary strategy checks
    return $product->getFinalPrice() > 0;
    ```

**Key Points**:

- Guards return `false` to **skip** strategy evaluation (allow the discount)
- Guards return `true` to **allow** strategy evaluation to proceed
- If **any** guard returns `false`, all strategies are skipped
- Guards have access to the full Rule object for sophisticated filtering

### 2. Discount Exclusion Strategies

[](#2-discount-exclusion-strategies)

**Purpose**: Strategies contain the actual business logic to determine if a product should be excluded from additional cart discounts because it already has a discount applied through another mechanism.

**When to use strategies**:

- Detect products with active special prices
- Check if catalog price rules are affecting the product
- Identify products with tier pricing
- Check for manufacturer promotions or wholesale pricing
- Any custom discount mechanism that should prevent stacking cart discounts

**Interface**:

```
namespace PixelPerfect\DiscountExclusion\Api;

interface DiscountExclusionStrategyInterface
{
    /**
     * Determines if a product should be excluded from cart discounts
     *
     * @param ProductInterface|Product $product The product to check
     * @param AbstractItem             $item    The quote item
     * @return bool True to exclude from cart discounts, false to allow other strategies to decide
     */
    public function shouldExcludeFromDiscount(
        ProductInterface|Product $product,
        AbstractItem $item
    ): bool;
}
```

**Built-in Strategies**:

1. **SpecialPriceStrategy**: Excludes products where the special price is active and equals the final price
2. **CatalogRuleStrategy**: Excludes products affected by catalog price rules

**Key Points**:

- Strategies return `true` to **exclude** the product from cart discounts
- Strategies return `false` to let other strategies decide
- Strategies are evaluated in order; **first match wins**
- Strategies only run if all guards returned `true`
- Strategies focus on "is this product already discounted?" logic

### 3. Max Discount Calculator

[](#3-max-discount-calculator)

**Purpose**: Computes the capped discount when a bypassed rule applies to an already-discounted product. The customer receives `max(existing discount, rule discount)` from the regular price.

**Interface**:

```
namespace PixelPerfect\DiscountExclusion\Api;

interface MaxDiscountCalculatorInterface
{
    /**
     * Calculate the max-discount result for a bypassed rule
     *
     * @param ProductInterface|Product $product The product (with prices loaded)
     * @param Rule                     $rule    The cart price rule being evaluated
     * @param float                    $qty     Item quantity in the cart
     * @return BypassResult
     */
    public function calculate(ProductInterface|Product $product, Rule $rule, float $qty): BypassResult;
}
```

**Supported rule types**:

- `by_percent` — percentage-based discount
- `by_fixed` — fixed amount discount
- `cart_fixed` / `buy_x_get_y` — returns `STACKING_FALLBACK` (full stacking, max-discount not applicable)

**Result types** (`BypassResultType` enum):

- `ADJUSTED` — Rule discount exceeds existing; apply only the difference
- `EXISTING_BETTER` — Existing discount is equal or greater; block the rule discount
- `STACKING_FALLBACK` — Rule type not supported for max-discount; allow full stacking

### 4. Exclusion Result Collector

[](#4-exclusion-result-collector)

**Purpose**: A singleton service that collects excluded and bypassed items during quote processing, enabling consolidated message display after all items are processed.

**Interface**:

```
namespace PixelPerfect\DiscountExclusion\Api;

interface ExclusionResultCollectorInterface
{
    // Exclusion tracking
    public function addExcludedItem(AbstractItem $item, string $reason, string $couponCode): void;
    public function hasExcludedItems(string $couponCode): bool;
    public function hasAnyExcludedItems(): bool;
    public function getExcludedItems(string $couponCode): array;
    public function getCouponCodes(): array;

    // Bypass tracking
    public function addBypassedItem(AbstractItem $item, BypassResultType $type, string $couponCode, array $messageParams = []): void;
    public function hasBypassedItems(string $couponCode): bool;
    public function hasAnyBypassedItems(): bool;
    public function getBypassedItems(string $couponCode): array;

    public function clear(): void;
}
```

**Key Points**:

- Collects excluded and bypassed items per coupon code
- Deduplicates by product ID (same product won't be added twice)
- Bypass items carry message parameters for rendering (discount percentages, amounts)
- Cleared after messages are displayed

### 5. Exclusion Message Builder

[](#5-exclusion-message-builder)

**Purpose**: Builds consolidated exclusion and bypass messages for a given coupon code. Supports both immediate display (via message manager) and deferred display (via session queuing).

**Interface**:

```
namespace PixelPerfect\DiscountExclusion\Api;

interface ExclusionMessageBuilderInterface
{
    // Add messages directly to the message manager
    public function addMessagesForCoupon(string $couponCode): void;

    // Build messages for session queuing (returns array of {type, text})
    public function buildMessagesForCoupon(string $couponCode): array;
}
```

**Message types**:

- **Exclusion warnings**: "Coupon X was not applied to Y because it is already discounted"
- **Bypass adjusted notices**: "Coupon X applied an additional 5% discount to Y, adjusted from 30% because it is already 25% discounted"
- **Bypass existing\_better warnings**: "Coupon X was not applied to Y because the existing 25% discount already exceeds the coupon's 20% discount"

### 6. Observers

[](#6-observers)

**CouponPostObserver**: Handles message display and coupon cleanup after coupon application

- Listens to `controller_action_postdispatch_checkout_cart_couponPost`
- Displays consolidated messages for all excluded and bypassed products
- Removes coupon from quote if no actual discount was applied
- Clears Magento's generic error messages and replaces with specific exclusion messages

**CartUpdateObserver**: Queues messages for display on the cart page

- Listens to cart add, update, and delete post-dispatch events
- Builds messages via `ExclusionMessageBuilder` and stores them in the checkout session
- Messages are only displayed when the cart page loads (not on PLP/PDP)

**CartPageLoadObserver**: Displays queued messages and clears session state on cart page load

- Listens to `controller_action_predispatch_checkout_cart_index`
- Reads queued messages from session and displays them
- Clears processed product IDs from session

Extending the Module
--------------------

[](#extending-the-module)

The module is designed to be extended through dependency injection. You can add your own guards and strategies without modifying core module code.

### Adding a Strategy Eligibility Guard

[](#adding-a-strategy-eligibility-guard)

**Step 1**: Create your guard class implementing `StrategyEligibilityGuardInterface`

```
