Overview
Building a discount system from scratch in Laravel means writing the same percentage-vs-fixed logic, expiry checks, and race-condition-safe redemption code over and over. Laravel Discount by Milwad Khosravi packages all of that into Eloquent models evaluated through a single LaravelDiscount facade.
Installation
The package requires PHP 8.1+ and Laravel 9–13.
composer require binafy/laravel-discount
php artisan migrate
Migrations create discounts, discount_usages, and discountables tables. Publish the config only if you need to change table names, the user model, or code-generation defaults:
php artisan vendor:publish --tag="laravel-discount-config"
Percentage and Fixed Discounts
A discount is a standard Eloquent model. Apply it through the facade and receive a DiscountResult:
use Binafy\LaravelDiscount\Enums\DiscountType;
use Binafy\LaravelDiscount\Models\Discount;
use Binafy\LaravelDiscount\Facades\LaravelDiscount;
$discount = Discount::query()->create([
'name' => 'Summer Sale',
'type' => DiscountType::Percentage,
'value' => 20,
]);
$result = LaravelDiscount::apply($discount, 200);
// originalAmount: 200.0 | discountAmount: 40.0 | payableAmount(): 160.0
Add max_discount_amount to cap a percentage discount — the classic "20% off, up to $100" scenario — without hardcoding anything in a controller.
Coupon Codes, Expiry, and Usage Limits
Setting a code column turns a discount into a promotional coupon. applyCode() validates the code and throws DiscountNotFoundException on a miss:
$result = LaravelDiscount::applyCode('WELCOME10', 200, $user);
Bulk codes are generated with random_int(), stripping ambiguous characters like 0/O and 1/I:
LaravelDiscount::generateCode(); // "8FJ2K9QW"
LaravelDiscount::generateCodes(100, 'VIP'); // Collection of 100 unique codes
Time windows use starts_at and expires_at. A valid() query scope filters to currently active discounts. Applying outside the window throws typed exceptions (DiscountNotStartedException, DiscountExpiredException) and fires a DiscountExpired event.
Usage limits are enforced at redemption time, not application time, using a database-level increment inside a transaction:
LaravelDiscount::redeem($discount, $user, $result->discountAmount);
If two customers claim the last redemption simultaneously, only one succeeds — the database decides. Guests are tracked via session ID instead of a user model.
Stackable Discounts and Model Attachment
Mark a discount is_stackable and applyMany() resolves the optimal combination automatically: it sums all stackable discounts, finds the single best non-stackable one, and returns whichever side saves more.
$result = LaravelDiscount::applyMany([$tenPercent, $tenFixed, $bigSolo], 100);
// $result->discounts — the codes that actually applied
// $result->discountAmount — the winning total
The HasDiscounts trait attaches discounts to any Eloquent model via a polymorphic relationship:
class Product extends Model
{
use HasDiscounts;
}
$product->discounts()->attach($discount);
$result = $product->applyDiscounts($product->price);
Cart Integration
Install the companion binafy/laravel-cart package and a CartDiscount service handles cart-level and item-level discounts:
$cartDiscount->applyToCart($cart, 'SUMMER-8FJ2K9QW'); // cart total
$cartDiscount->applyToItem($cartItem, $discount); // single line item
$cartDiscount->applyItemDiscounts($cart); // all items via HasDiscounts
Validation and Exceptions
A ValidDiscountCode form-request rule rejects invalid codes before any business logic runs, returning a specific failure reason rather than a generic message. Every failure case has its own exception extending DiscountException, each exposing the failing discount via getDiscount():
try {
$result = LaravelDiscount::applyCode($code, $total, $user);
} catch (DiscountExpiredException $e) {
return back()->withErrors("Code {$e->getDiscount()->code} has expired.");
} catch (DiscountException $e) {
return back()->withErrors($e->getMessage());
}
Three lifecycle events — DiscountApplied, DiscountRedeemed, and DiscountExpired — carry enough data to drive analytics without additional queries.
Key Takeaways
- Two discount types (
Percentage,Fixed) with an optionalmax_discount_amountcap. - Race-condition-safe redemption via database-level transactional increments.
- Guest support through session ID tracking alongside nullable
user_id. - Stacking resolver automatically picks the combination that saves the customer the most.
HasDiscountstrait attaches discounts polymorphically to any Eloquent model.- Artisan commands
discount:generateanddiscount:prunefor terminal and scheduler use.
Source: Laravel Discount: Coupon Codes, Usage Limits, and Stacking — Laravel News