Models
Shopper uses two models to manage discounts:| Model | Purpose |
|---|---|
Shopper\Core\Models\Discount | The discount definition (code, type, value, rules) |
Shopper\Core\Models\DiscountDetail | Polymorphic link connecting discounts to specific products or customers |
Shopper\Core\Models\Contracts\Discount. Neither model is configurable via config/shopper/models.php.
Database Schema
Discount Table
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | bigint | no | auto | Primary key |
code | string | no | - | Unique discount code |
type | string | no | - | Discount type enum |
value | integer | no | - | Discount value (percentage or cents) |
is_active | boolean | no | false | Discount visibility |
apply_to | string | no | - | Application scope enum |
min_required | string | no | - | Requirement type enum |
min_required_value | string | yes | null | Minimum value required |
eligibility | string | no | - | Eligibility type enum |
usage_limit | integer | yes | null | Maximum total uses |
usage_limit_per_user | boolean | no | false | Limit one use per customer |
total_use | integer | no | 0 | Current total usage count |
start_at | datetime | no | - | Discount start date |
end_at | datetime | yes | null | Discount end date (null = no expiry) |
zone_id | bigint | yes | null | FK to zone (null = all zones) |
metadata | json | yes | null | Additional custom data |
created_at | timestamp | yes | null | Creation timestamp |
updated_at | timestamp | yes | null | Last update timestamp |
Discountable Table (Pivot)
| Column | Type | Nullable | Description |
|---|---|---|---|
id | bigint | no | Primary key |
condition | string | yes | Condition type (apply_to, eligibility) |
total_use | integer | no | Usage count for this relation |
discountable_id | bigint | no | Polymorphic relation ID |
discountable_type | string | no | Polymorphic relation type |
discount_id | bigint | no | FK to discount |
Discount Type
TheDiscountType enum defines value types:
| Type | Database Value | Value Handling |
|---|---|---|
| Percentage | percentage | Stored as-is (10 = 10%) |
| Fixed Amount | fixed_amount | Stored in cents (1000 = $10.00) |
Discount Application
TheDiscountApplyTo enum defines where the discount applies:
| Scope | Database Value | Description |
|---|---|---|
| Order | order | Discount applies to total order amount |
| Products | products | Discount applies only to linked products |
Discount Eligibility
TheDiscountEligibility enum defines who can use the discount:
| Eligibility | Database Value | Description |
|---|---|---|
| Everyone | everyone | Any customer can use |
| Customers | customers | Only linked customers can use |
Discount Requirements
TheDiscountRequirement enum defines minimum conditions:
| Requirement | Database Value | min_required_value |
|---|---|---|
| None | none | Not used |
| Price | price | Minimum amount in cents |
| Quantity | quantity | Minimum item count |
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.
Helpers
Handling DiscountLimitReachedException
Wrap your checkout flow to surface a friendly message to the customer when either limit is hit at commit time:| Constructor | Thrown When |
|---|---|
DiscountLimitReachedException::global($code) | Global usage_limit was reached between cart validation and order commit |
DiscountLimitReachedException::perUser($code) | Customer already redeemed a discount with usage_limit_per_user set |
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:| Permission | Description |
|---|---|
browse_discounts | View the discounts list |
read_discounts | View a single discount |
add_discounts | Create new discounts |
edit_discounts | Edit existing discounts |
delete_discounts | Delete discounts |
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):
Use Cases
| Scenario | Type | Apply To | Eligibility | Requirement |
|---|---|---|---|---|
| Site-wide sale | Percentage | Order | Everyone | None |
| Free shipping over $50 | Fixed | Order | Everyone | Price |
| Buy 3+ get 10% off | Percentage | Order | Everyone | Quantity |
| VIP member exclusive | Percentage | Order | Customers | None |
| Product clearance | Percentage | Products | Everyone | None |
| First order discount | Fixed | Order | Customers | None |