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
TheDiscountType enum defines value types:
Discount Application
TheDiscountApplyTo enum defines where the discount applies:
Discount Eligibility
TheDiscountEligibility enum defines who can use the discount:
Discount Requirements
TheDiscountRequirement enum defines minimum conditions:
Discount Condition
TheDiscountCondition 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, store1000 (cents). For a 15% percentage discount, store 15:
Usage Limit Enforcement
Discounts can cap the total number of redemptions withusage_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
Theusage_limit column caps the total number of redemptions across all customers. The total_use counter is incremented atomically inside the CreateOrderFromCartAction transaction:
- The discount row is locked with
lockForUpdateto serialize concurrent checkouts. - A conditional
UPDATEincrementstotal_useonly if it is still belowusage_limit. - 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.
Per-User Limit
Whenusage_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:
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: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 viaCartManager::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 inconfig/shopper/features.php:
Permissions
The admin panel generates five permissions for discount management:Components
To customize the admin UI for discount management:config/shopper/components/discount.php:
Storefront Example
Applying a discount code on the storefront uses the Cart package API. TheCartManager::applyCoupon() method validates the code exists, and the calculation pipeline handles the rest (validation rules, discount calculation, and adjustment creation):