Skip to main content
Discounts allow you to create promotional offers for your customers. Shopper supports percentage and fixed amount discounts with flexible application rules, eligibility conditions, and usage limits.

Models

Shopper uses two models to manage discounts: The Discount model implements Shopper\Core\Models\Contracts\Discount. Neither model is configurable via config/shopper/models.php.

Database Schema

Discount Table

Discountable Table (Pivot)

Discount Type

The DiscountType enum defines value types:

Discount Application

The DiscountApplyTo enum defines where the discount applies:

Discount Eligibility

The DiscountEligibility enum defines who can use the discount:

Discount Requirements

The DiscountRequirement enum defines minimum conditions:

Discount Condition

The DiscountCondition enum is used on the DiscountDetail pivot model to distinguish whether a linked record represents a product (the discount applies to) or a customer (who is eligible):

Relationships

Items (DiscountDetail)

Zone

Value Handling

Fixed amount values are stored in cents. Percentage values are stored as the percentage number. For a $10 fixed discount, store 1000 (cents). For a 15% percentage discount, store 15:

Usage Limit Enforcement

Discounts can cap the total number of redemptions with usage_limit and restrict each customer to one redemption with usage_limit_per_user. Both limits are enforced with strong consistency guarantees so that a coupon can never be silently over-redeemed under concurrent checkout.

Global Usage Limit

The usage_limit column caps the total number of redemptions across all customers. The total_use counter is incremented atomically inside the CreateOrderFromCartAction transaction:
  1. The discount row is locked with lockForUpdate to serialize concurrent checkouts.
  2. A conditional UPDATE increments total_use only if it is still below usage_limit.
  3. If the conditional update affects zero rows, the limit was exhausted between cart validation and commit. DiscountLimitReachedException::global() is thrown and the order transaction rolls back, so no order is created.
This compare-and-swap pattern is what fixes the silent over-redemption issue that was patched in v2.8.

Per-User Limit

When usage_limit_per_user is true, each customer can redeem the discount at most once. The check counts prior orders on the orders.discount_id column (introduced in v2.8), not the legacy DiscountDetail.total_use counter:
The same query runs in two places. DiscountValidator rejects the code at cart-apply time so the customer is not surprised at checkout. CreateOrderFromCartAction re-checks at commit and throws DiscountLimitReachedException::perUser() if the customer redeemed it between cart apply and commit. A per-user limit is meaningless without an identified customer, so a guest cart (no customer_id) can never apply a discount that sets usage_limit_per_user. Both DiscountValidator and CreateOrderFromCartAction reject it with the discount.requires_login error rather than letting a guest redeem a once-per-customer code on every anonymous checkout.

Helpers

Handling DiscountLimitReachedException

Wrap your checkout flow to surface a friendly message to the customer when either limit is hit at commit time:
The exception has two static constructors that match the two limit types:

Creating Discounts

Percentage Discount for All Orders

Fixed Amount Discount with Minimum Order

Product-Specific Discount

VIP Customer Discount

Retrieving Discounts

Discount Validation

When a coupon is applied to a cart via CartManager::applyCoupon(), the Cart package validates the discount automatically during the calculation pipeline. You do not need to build manual validation logic. The built-in DiscountValidator checks all rules (active status, date range, usage limits, eligibility, zone, minimum amounts) and produces clear error messages. For cases outside the cart pipeline where you need to check a discount programmatically:

Configuration

Disabling Discounts

If your store doesn’t use discount codes, disable the feature in config/shopper/features.php:

Permissions

The admin panel generates five permissions for discount management:

Components

To customize the admin UI for discount management:
Creates config/shopper/components/discount.php:

Storefront Example

Applying a discount code on the storefront uses the Cart package API. The CartManager::applyCoupon() method validates the code exists, and the calculation pipeline handles the rest (validation rules, discount calculation, and adjustment creation):

Use Cases