Skip to main content
Lunar provides a flexible discount system supporting coupons, percentage and fixed reductions, and buy-x-get-y promotions.

Overview

Lunar provides a discount system that supports multiple discount types out of the box, including amount-off (percentage or fixed) and buy-x-get-y promotions. Discounts can be scoped to specific channels, customer groups, collections, brands, and individual products or variants.
Lunar\Models\Discount
FieldTypeDescription
idbigIncrementsPrimary key
namestring
handlestringUnique identifier
couponstring nullableCoupon code customers can enter to apply the discount
typestringFully qualified class name of the discount type
starts_atdateTimeWhen the discount becomes active
ends_atdateTime nullableWhen the discount expires, if null the discount does not expire
usesunsignedIntegerHow many times the discount has been used
max_usesunsignedMediumInteger nullableMaximum times this discount can be applied storewide
max_uses_per_userunsignedMediumInteger nullableMaximum times a single user can use this discount
priorityunsignedMediumIntegerOrder of priority (default: 1)
stopbooleanWhether to stop processing further discounts after this one is applied
restrictionstring nullableRestriction type
datajson nullableDiscount type-specific configuration data
created_attimestamp
updated_attimestamp

Relationships

RelationshipTypeRelated ModelDescription
usersBelongsToManyUserUsers who have used this discount
customersBelongsToManyLunar\Models\CustomerCustomers the discount is restricted to
customerGroupsBelongsToManyLunar\Models\CustomerGroupCustomer groups the discount is available to
channelsMorphToManyLunar\Models\ChannelChannels the discount is available on
collectionsBelongsToManyLunar\Models\CollectionCollections associated with the discount
brandsBelongsToManyLunar\Models\BrandBrands associated with the discount
discountablesHasManyLunar\Models\DiscountableAll discountable entries (conditions, exclusions, limitations, rewards)
discountableConditionsHasManyLunar\Models\DiscountableProducts or variants that must be in the cart to activate the discount
discountableExclusionsHasManyLunar\Models\DiscountableProducts or variants excluded from the discount
discountableLimitationsHasManyLunar\Models\DiscountableProducts or variants the discount is limited to
discountableRewardsHasManyLunar\Models\DiscountableReward products or variants (used by BuyXGetY)

Scopes

ScopeDescription
active()Filters to discounts that have started and have not expired
usable()Filters to discounts where uses is less than max_uses, or max_uses is null
products($productIds, $types)Filters by associated product IDs and discountable types
productVariants($variantIds, $types)Filters by associated variant IDs and discountable types
collections($collectionIds, $types)Filters by associated collection IDs and discountable types
brands($brandIds, $types)Filters by associated brand IDs and discountable types
channel($channel)Filters by channel
customerGroup($customerGroup)Filters by customer group

Creating a Discount

use Lunar\Models\Discount;

Discount::create([
    'name' => '20% Coupon',
    'handle' => '20_coupon',
    'coupon' => '20OFF',
    'type' => 'Lunar\DiscountTypes\AmountOff',
    'data' => [
        'fixed_value' => false,
        'percentage' => 20,
        'min_prices' => [
            'USD' => 2000, // $20.00 minimum spend
        ],
    ],
    'starts_at' => '2024-01-01 00:00:00',
    'ends_at' => null,
    'max_uses' => null,
]);

Discount Status

The Discount model provides a status attribute that returns the current state of the discount based on its dates and usage.
use Lunar\Models\Discount;

$discount = Discount::find(1);

$discount->status; // 'active', 'pending', 'expired', or 'scheduled'
StatusDescription
activeThe discount has started and has not expired
pendingThe discount has not started yet
expiredThe discount has passed its ends_at date
scheduledThe discount is scheduled for a future date

Resetting the Discount Cache

For performance reasons, applicable discounts are cached per request. To reset this cache (for example, after adding a discount code to a cart), call resetDiscounts() on the Discounts facade:
use Lunar\Facades\Discounts;

Discounts::resetDiscounts();

Validating Coupons

The Discounts facade provides a method to validate whether a coupon code is valid:
use Lunar\Facades\Discounts;

$isValid = Discounts::validateCoupon('20OFF');
The default coupon validator checks that the coupon exists on an active, usable discount. The validator class can be customized in the config/lunar/discounts.php configuration file:
return [
    'coupon_validator' => \Lunar\Base\Validation\CouponValidator::class,
];

Discountable

The Discountable model links products, product variants, or collections to a discount. Each entry has a type that determines its role.
Lunar\Models\Discountable
FieldTypeDescription
idbigIncrementsPrimary key
discount_idforeignId
discountable_typestringMorph type (e.g., product, product_variant, collection)
discountable_idunsignedBigIntegerMorph ID
typestringThe role: condition, exclusion, limitation, or reward
created_attimestamp
updated_attimestamp
The type field determines how the discountable relates to the discount:
  • condition — The product or variant must be in the cart for the discount to activate.
  • exclusion — The product or variant is excluded from the discount.
  • limitation — The discount only applies to these products or variants.
  • reward — These products or variants are given as the reward (used by BuyXGetY).

Relationships

RelationshipTypeRelated ModelDescription
discountBelongsToLunar\Models\DiscountThe parent discount
discountableMorphToProduct, ProductVariant, or CollectionThe associated purchasable or collection

Built-in Discount Types

Lunar ships with two discount types. Both extend Lunar\DiscountTypes\AbstractDiscountType.

AmountOff

Lunar\DiscountTypes\AmountOff
Applies either a percentage or fixed amount discount to eligible cart lines. The data column stores the discount configuration: Percentage discount:
use Lunar\Models\Discount;

Discount::create([
    'name' => '10% Off',
    'handle' => '10_percent_off',
    'type' => 'Lunar\DiscountTypes\AmountOff',
    'data' => [
        'fixed_value' => false,
        'percentage' => 10,
        'min_prices' => [
            'USD' => 5000, // $50.00 minimum spend
        ],
    ],
    'starts_at' => now(),
    'ends_at' => null,
]);
Fixed value discount:
use Lunar\Models\Discount;

Discount::create([
    'name' => '$5 Off',
    'handle' => '5_dollars_off',
    'type' => 'Lunar\DiscountTypes\AmountOff',
    'data' => [
        'fixed_value' => true,
        'fixed_values' => [
            'USD' => 500, // $5.00 off (stored in minor units)
            'EUR' => 450,
        ],
    ],
    'starts_at' => now(),
    'ends_at' => null,
]);

BuyXGetY

Lunar\DiscountTypes\BuyXGetY
Allows “buy X, get Y free” style promotions. Condition products are defined through the discountableConditions relationship, and reward products through the discountableRewards relationship.
use Lunar\Models\Discount;

$discount = Discount::create([
    'name' => 'Buy 2 Get 1 Free',
    'handle' => 'buy_2_get_1',
    'type' => 'Lunar\DiscountTypes\BuyXGetY',
    'data' => [
        'min_qty' => 2,
        'reward_qty' => 1,
        'max_reward_qty' => 5,
        'automatically_add_rewards' => false,
    ],
    'starts_at' => now(),
    'ends_at' => null,
]);
Data FieldTypeDescription
min_qtyintegerMinimum quantity of condition products required to trigger the discount
reward_qtyintegerNumber of reward items per qualifying group
max_reward_qtyintegerMaximum total reward items (optional)
automatically_add_rewardsbooleanWhether to automatically add reward items to the cart

Custom Discount Types

Custom discount types can be created by extending Lunar\DiscountTypes\AbstractDiscountType:
<?php

namespace App\DiscountTypes;

use Lunar\Models\Contracts\Cart;
use Lunar\DiscountTypes\AbstractDiscountType;

class CustomDiscountType extends AbstractDiscountType
{
    /**
     * Return the name of the discount type.
     */
    public function getName(): string
    {
        return 'Custom Discount Type';
    }

    /**
     * Apply the discount to the cart.
     */
    public function apply(Cart $cart): Cart
    {
        // Custom discount logic...
        return $cart;
    }
}
Register the custom type using the Discounts facade, typically in a service provider:
use Lunar\Facades\Discounts;
use App\DiscountTypes\CustomDiscountType;

Discounts::addType(CustomDiscountType::class);

Discounts Facade

The Lunar\Facades\Discounts facade provides methods for managing and applying discounts:
use Lunar\Facades\Discounts;
MethodReturnsDescription
channel($channel)DiscountManagerSet the channel(s) for discount filtering
customerGroup($group)DiscountManagerSet the customer group(s) for discount filtering
getChannels()CollectionGet the currently set channels
getCustomerGroups()CollectionGet the currently set customer groups
getDiscounts()CollectionGet available discounts for the current channels and groups
addType($type)DiscountManagerRegister a custom discount type
getTypes()CollectionGet all registered discount types
apply($cart)CartApply all relevant discounts to a cart
getApplied()CollectionGet the discounts that were applied
resetDiscounts()DiscountManagerClear the cached discounts
validateCoupon($coupon)boolValidate whether a coupon code is valid and usable