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:
ModelPurpose
Shopper\Core\Models\DiscountThe discount definition (code, type, value, rules)
Shopper\Core\Models\DiscountDetailPolymorphic link connecting discounts to specific products or customers
The Discount model implements Shopper\Core\Models\Contracts\Discount. Neither model is configurable via config/shopper/models.php.

Database Schema

Discount Table

ColumnTypeNullableDefaultDescription
idbigintnoautoPrimary key
codestringno-Unique discount code
typestringno-Discount type enum
valueintegerno-Discount value (percentage or cents)
is_activebooleannofalseDiscount visibility
apply_tostringno-Application scope enum
min_requiredstringno-Requirement type enum
min_required_valuestringyesnullMinimum value required
eligibilitystringno-Eligibility type enum
usage_limitintegeryesnullMaximum total uses
usage_limit_per_userbooleannofalseLimit one use per customer
total_useintegerno0Current total usage count
start_atdatetimeno-Discount start date
end_atdatetimeyesnullDiscount end date (null = no expiry)
zone_idbigintyesnullFK to zone (null = all zones)
metadatajsonyesnullAdditional custom data
created_attimestampyesnullCreation timestamp
updated_attimestampyesnullLast update timestamp

Discountable Table (Pivot)

ColumnTypeNullableDescription
idbigintnoPrimary key
conditionstringyesCondition type (apply_to, eligibility)
total_useintegernoUsage count for this relation
discountable_idbigintnoPolymorphic relation ID
discountable_typestringnoPolymorphic relation type
discount_idbigintnoFK to discount

Discount Type

The DiscountType enum defines value types:
use Shopper\Core\Enum\DiscountType;

DiscountType::Percentage   // percentage - e.g., 10% off
DiscountType::FixedAmount  // fixed_amount - e.g., $10 off
TypeDatabase ValueValue Handling
PercentagepercentageStored as-is (10 = 10%)
Fixed Amountfixed_amountStored in cents (1000 = $10.00)

Discount Application

The DiscountApplyTo enum defines where the discount applies:
use Shopper\Core\Enum\DiscountApplyTo;

DiscountApplyTo::Order     // order - Applies to entire order
DiscountApplyTo::Products  // products - Applies to specific products
ScopeDatabase ValueDescription
OrderorderDiscount applies to total order amount
ProductsproductsDiscount applies only to linked products

Discount Eligibility

The DiscountEligibility enum defines who can use the discount:
use Shopper\Core\Enum\DiscountEligibility;

DiscountEligibility::Everyone   // everyone - All customers
DiscountEligibility::Customers  // customers - Specific customers only
EligibilityDatabase ValueDescription
EveryoneeveryoneAny customer can use
CustomerscustomersOnly linked customers can use

Discount Requirements

The DiscountRequirement enum defines minimum conditions:
use Shopper\Core\Enum\DiscountRequirement;

DiscountRequirement::None     // none - No minimum
DiscountRequirement::Price    // price - Minimum order amount
DiscountRequirement::Quantity // quantity - Minimum items count
RequirementDatabase Valuemin_required_value
NonenoneNot used
PricepriceMinimum amount in cents
QuantityquantityMinimum item count

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):
use Shopper\Core\Enum\DiscountCondition;

DiscountCondition::ApplyTo      // apply_to - Links a product to the discount
DiscountCondition::Eligibility  // eligibility - Links a customer to the discount

Relationships

Items (DiscountDetail)

use Shopper\Core\Enum\DiscountCondition;

// Get all discount items (products or customers)
$discount->items; // Collection of DiscountDetail models

// Add a product to discount
$discount->items()->create([
    'condition' => DiscountCondition::ApplyTo,
    'discountable_type' => Product::class,
    'discountable_id' => $productId,
]);

// Add eligible customer
$discount->items()->create([
    'condition' => DiscountCondition::Eligibility,
    'discountable_type' => User::class,
    'discountable_id' => $userId,
]);

Zone

// Get discount zone (market)
$discount->zone; // Zone model or null

// Restrict discount to a zone
$discount->update(['zone_id' => $zoneId]);

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:
Discount::query()->create([
    'code' => 'SAVE10',
    'type' => DiscountType::FixedAmount,
    'value' => 1000, // 1000 cents = $10.00
]);

Discount::query()->create([
    'code' => 'SAVE15',
    'type' => DiscountType::Percentage,
    'value' => 15, // 15%
]);

Usage Limit Checking

// Check if discount has reached its usage limit
$discount->hasReachedLimit(); // true if total_use === usage_limit

// Check remaining uses
if ($discount->usage_limit !== null) {
    $remaining = $discount->usage_limit - $discount->total_use;
}

Creating Discounts

Percentage Discount for All Orders

use Shopper\Core\Models\Discount;
use Shopper\Core\Enum\DiscountType;
use Shopper\Core\Enum\DiscountApplyTo;
use Shopper\Core\Enum\DiscountEligibility;
use Shopper\Core\Enum\DiscountRequirement;

$discount = Discount::query()->create([
    'code' => 'SUMMER20',
    'type' => DiscountType::Percentage,
    'value' => 20, // 20% off
    'is_active' => true,
    'apply_to' => DiscountApplyTo::Order,
    'eligibility' => DiscountEligibility::Everyone,
    'min_required' => DiscountRequirement::None,
    'start_at' => now(),
    'end_at' => now()->addMonth(),
]);

Fixed Amount Discount with Minimum Order

$discount = Discount::query()->create([
    'code' => 'SAVE10',
    'type' => DiscountType::FixedAmount,
    'value' => 1000, // 1000 cents = $10 off
    'is_active' => true,
    'apply_to' => DiscountApplyTo::Order,
    'eligibility' => DiscountEligibility::Everyone,
    'min_required' => DiscountRequirement::Price,
    'min_required_value' => '5000', // Minimum 5000 cents = $50 order
    'start_at' => now(),
    'usage_limit' => 100, // Max 100 uses
]);

Product-Specific Discount

$discount = Discount::query()->create([
    'code' => 'PRODUCT15',
    'type' => DiscountType::Percentage,
    'value' => 15,
    'is_active' => true,
    'apply_to' => DiscountApplyTo::Products,
    'eligibility' => DiscountEligibility::Everyone,
    'min_required' => DiscountRequirement::None,
    'start_at' => now(),
]);

// Link specific products
$discount->items()->createMany([
    [
        'condition' => 'apply_to',
        'discountable_type' => Product::class,
        'discountable_id' => $product1->id,
    ],
    [
        'condition' => 'apply_to',
        'discountable_type' => Product::class,
        'discountable_id' => $product2->id,
    ],
]);

VIP Customer Discount

$discount = Discount::query()->create([
    'code' => 'VIP30',
    'type' => DiscountType::Percentage,
    'value' => 30,
    'is_active' => true,
    'apply_to' => DiscountApplyTo::Order,
    'eligibility' => DiscountEligibility::Customers,
    'min_required' => DiscountRequirement::None,
    'start_at' => now(),
    'usage_limit_per_user' => true, // One use per customer
]);

// Link eligible customers
foreach ($vipCustomers as $customer) {
    $discount->items()->create([
        'condition' => 'eligibility',
        'discountable_type' => User::class,
        'discountable_id' => $customer->id,
    ]);
}

Retrieving Discounts

use Shopper\Core\Models\Discount;

// Get all active discounts
$discounts = Discount::query()
    ->where('is_active', true)
    ->where('start_at', '<=', now())
    ->where(function ($q) {
        $q->whereNull('end_at')
            ->orWhere('end_at', '>', now());
    })
    ->get();

// Get discount by code
$discount = Discount::query()
    ->where('code', 'SUMMER20')
    ->where('is_active', true)
    ->first();

// Get discounts for a zone
$discounts = Discount::query()
    ->where('is_active', true)
    ->where(function ($q) use ($zoneId) {
        $q->whereNull('zone_id')
            ->orWhere('zone_id', $zoneId);
    })
    ->get();

// Get discounts with available uses
$discounts = Discount::query()
    ->where('is_active', true)
    ->where(function ($q) {
        $q->whereNull('usage_limit')
            ->orWhereColumn('total_use', '<', 'usage_limit');
    })
    ->get();

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:
use Shopper\Core\Models\Discount;

$discount = Discount::query()
    ->where('code', $code)
    ->where('is_active', true)
    ->first();

if ($discount && ! $discount->hasReachedLimit()) {
    // Discount is valid and has remaining uses
}

Configuration

Disabling Discounts

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

return [
    'discount' => FeatureState::Disabled,
];

Permissions

The admin panel generates five permissions for discount management:
PermissionDescription
browse_discountsView the discounts list
read_discountsView a single discount
add_discountsCreate new discounts
edit_discountsEdit existing discounts
delete_discountsDelete discounts

Components

To customize the admin UI for discount management:
php artisan shopper:component:publish discount
Creates config/shopper/components/discount.php:
use Shopper\Livewire;

return [
    'pages' => [
        'discount-index' => Livewire\Pages\Discount\Index::class,
    ],
    'components' => [
        'slide-overs.discount-form' => Livewire\SlideOvers\DiscountForm::class,
    ],
];

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):
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Shopper\Cart\CartManager;
use Shopper\Cart\Exceptions\InvalidDiscountException;
use Shopper\Cart\Facades\Cart;

class CouponController extends Controller
{
    public function apply(Request $request)
    {
        $request->validate(['code' => 'required|string']);

        $manager = resolve(CartManager::class);

        try {
            $manager->applyCoupon(Cart::current(), $request->code);
        } catch (InvalidDiscountException $e) {
            return back()->with('error', 'Invalid discount code.');
        }

        return back()->with('success', 'Discount applied.');
    }

    public function remove()
    {
        resolve(CartManager::class)->removeCoupon(Cart::current());

        return back()->with('success', 'Discount removed.');
    }
}

Use Cases

ScenarioTypeApply ToEligibilityRequirement
Site-wide salePercentageOrderEveryoneNone
Free shipping over $50FixedOrderEveryonePrice
Buy 3+ get 10% offPercentageOrderEveryoneQuantity
VIP member exclusivePercentageOrderCustomersNone
Product clearancePercentageProductsEveryoneNone
First order discountFixedOrderCustomersNone