data column.
Overview
ALunar\Core\Models\Discount record describes when a discount is active, who it is available to, and what it targets. The type column names a class implementing Lunar\Core\Contracts\DiscountType that decides how the discount is applied to a cart. Lunar ships three types: percentage off, fixed amount off, and buy-x-get-y.
coupon is cast through Lunar\Core\Casts\CouponString, which normalizes the stored value to uppercase.
Relationships
Scopes
The status attribute
Targeting: Discountable
Lunar\Core\Models\Discountable links a product, product variant, or collection to a discount, tagged with a role via its type column.
condition: must be in the cart for the discount to activate (used by buy-x-get-y for the “buy” side).exclusion: excluded from a percentage-off or fixed-amount-off discount’s eligible lines.limitation: a percentage-off or fixed-amount-off discount only applies to these products or variants.reward: given as the reward by a buy-x-get-y discount.
Relationships
Scopes
Percentage-off and fixed-amount-off discounts also target lines by collection or brand directly through the
Discount::collections() and Discount::brands() relationships (pivot type of limitation or exclusion), rather than through Discountable.
Built-in discount types
Every discount type extendsLunar\Core\DiscountTypes\AbstractDiscountType and implements Lunar\Core\Contracts\DiscountType:
AbstractDiscountType provides shared behavior every type relies on:
checkDiscountConditions(Cart $cart): bool: validates the discount against the cart before it applies anything, including a matching coupon code (if the discount requires one), the cart customer againstcustomers()(if restricted), adata.min_pricesminimum spend for the cart’s currency,max_uses, andmax_uses_per_user. Every shipped type calls this at the top ofapply().markAsUsed(Cart $cart): incrementsusesand attaches the cart’s user.addDiscountBreakdown(Cart $cart, DiscountBreakdown $breakdown): records how much this discount took off, and which lines it affected, on$cart->discountBreakdown.
data.min_prices is keyed by currency code and stored in minor units, and applies regardless of which type is used:
PercentageOff
FixedAmountOff
PercentageOff and FixedAmountOff both narrow the cart’s eligible lines through Lunar\Core\DiscountTypes\Concerns\TargetsCartLines, which applies the discount’s collection, brand, and Discountable limitations and exclusions before any amount is calculated.
Prior to v2, this was a single
AmountOff type switched by a data.fixed_value boolean. Every stored discount of that type is migrated by the upgrade package to either PercentageOff or FixedAmountOff, and data.fixed_values is renamed to data.amounts.BuyXGetY
discountableConditions, reward products (the “get” side) from discountableRewards. Both accept products, product variants, or collections.
Condition and reward matching also accepts collections: a
Discountable entry with discountable_type set to a collection matches any cart line whose product belongs to that collection.
Automatically adding rewards
Whenautomatically_add_rewards is true and a qualifying cart does not already contain enough reward items, BuyXGetY adds a cart line for the reward rather than requiring the customer to add it themselves:
- A reward candidate is chosen at random from the fulfillable
discountableRewards(a collection reward picks a random fulfillable product from that collection). - Only reward items that can currently be fulfilled at the required quantity are considered; an out-of-stock reward is skipped rather than raising an error.
- A reward quantity greater than one is added to a single cart line, not one line per unit.
- Added lines are tracked in the line’s
meta.added_by_discountfor the discount’s ID and quantity.
The Discounts facade
Lunar\Core\Facades\Discounts resolves Lunar\Core\Contracts\DiscountManager (bound scoped, so a long-lived worker gets a fresh instance per request or job):
getDiscounts() and apply() memoize the resolved discount list per cart state (cart ID, coupon code, customer, and line purchasables). Call resetDiscounts() after a change that should re-evaluate eligibility, for example after a coupon code is applied to the cart.
Registering a custom discount type
boot() method. See Extending Discounts for how to build a custom discount type.
Validating coupons
Lunar\Core\Validation\CouponValidator, checks that the coupon matches an active discount under its max_uses and, if the discount sets max_uses_per_user, that the authenticated user has not exceeded it. Bind a different implementation of Lunar\Core\Contracts\CouponValidator in your own service provider to customize this.
How discounts apply in the cart pipeline
Eligibility is narrowed in two stages. First,getDiscounts() pre-filters candidate discounts at the database level: when a cart is given, only discounts whose Discountable entries (condition or limitation) match the cart’s products or variants, or whose collections()/brands() pivot type is condition and matches a collection or brand on a cart line, are loaded at all. Second, each loaded discount’s own apply() re-checks the full set of conditions (checkDiscountConditions()) and, for PercentageOff/FixedAmountOff, narrows to eligible lines via TargetsCartLines (limitation/exclusion on collections, brands, and Discountable entries).
Discounts are applied as one step in the cart calculation pipeline (lunar.cart.pipelines.calculate), after lines and shipping are calculated and before tax:
Lunar\Core\Pipelines\Cart\ApplyDiscounts resets $cart->discounts and $cart->discountBreakdown, then calls Discounts::apply($cart), which runs each eligible discount’s type in turn. Each type mutates the affected CartLine totals (discountTotal, subTotalDiscounted) directly and, if it applied, records a Lunar\Core\ValueObjects\Cart\DiscountBreakdown describing what happened:
Because tax is calculated after discounts, discount amounts reduce the tax-relevant subtotal for the affected lines.
Custom discount types
A custom discount type extendsLunar\Core\DiscountTypes\AbstractDiscountType:
DiscountBreakdown, and register the type.