NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Coupons for Laravel

The Coupon model

RoundlyConsulting\Coupons\Models\Coupon is a regular Eloquent model with HasFactory and SoftDeletes. It isn’t final, so you can subclass it (see Extending).

use RoundlyConsulting\Coupons\Models\Coupon;

$coupon = Coupon::query()->where('code', 'SAVE20')->firstOrFail();

$coupon->activate();          // activated_at = now (pass a CarbonInterface to schedule)
$coupon->expire($expiresAt);  // expires_at (no argument = now)
$coupon->setMaxUsageTo(50);
$coupon->save();

$coupon->isActive();                 // bool
$coupon->isExpired();                // bool
$coupon->isAtMaximumUsage();         // bool (a 0 cap is unlimited)
$coupon->hasBeenUsedAtLeastOnce();   // bool
$coupon->canBeApplied();             // active, not expired, not at max usage

State checks

MethodTrue when / returns
isActive(): boolactivated_at is set and in the past.
isExpired(): boolexpires_at is set and in the past.
canBeApplied(): boolActive, not expired and not at max usage.
isAtMaximumUsage(): boolA positive max_usage has been reached (0 = unlimited).
isAtMaximumUsageFor(Model $redeemer): boolA positive max_usage_per_redeemer has been reached for that redeemer.
usageBy(Model $redeemer): intTracked redemptions by that redeemer.
hasBeenUsedAtLeastOnce(): boolusage > 0.
isFreeShipping() / appliesToShipping(): boolThe type is FreeShipping — zero your own shipping line.
appliesToCurrency(Money $price): boolUnlocked, or locked to the price’s currency.
meetsMinimumSpend(Money $price): boolNo minimum, or price >= minimum_spend. Throws money’s CurrencyMismatch across currencies — check appliesToCurrency() first.

Discount math

Three methods turn a coupon into money. The discount is always between zero and the price and never above max_discount, so a total can never go negative:

$coupon->discountFor($price);   // Money — 0 <= discount <= price, never above max_discount
$coupon->apply($price);         // Money — the price minus the discount, never negative
$coupon->discount($currency);   // money Discount labelled with the code, for a DiscountStack

Rounding is money-for-laravel’s — half away from zero by default. A few worked examples:

use RoundlyConsulting\Coupons\Models\Coupon;
use RoundlyConsulting\Money\Money;

$quarter = Coupon::factory()->percentage(2500)->make();         // 25 %
$quarter->apply(Money::ofMinor(1000, 'EUR'));                    // 7.50 EUR

$eighth = Coupon::factory()->percentage(1250)->make();          // 12.5 %
$eighth->discountFor(Money::ofMinor(999, 'EUR'));                // 1.25 EUR (124.875 minor units, half away from zero)

$capped = Coupon::factory()->cappedPercentage(5000, Money::ofMinor(300, 'EUR'))->make();
$capped->discountFor(Money::ofMinor(1000, 'EUR'));               // 3.00 EUR — 50 %, capped

$big = Coupon::factory()->fixed(5000, 'EUR')->make();           // 50.00 EUR off
$big->discountFor(Money::ofMinor(1000, 'EUR'));                  // 10.00 EUR — never more than the price
$big->apply(Money::ofMinor(1000, 'EUR'));                        // 0.00 EUR

$yen = Coupon::factory()->fixed(500, 'JPY')->make();
$yen->discountFor(Money::ofMinor(1200, 'JPY'));                  // 500 JPY

discountFor() and apply() throw money’s CurrencyMismatch when the coupon is locked to another currency — check appliesToCurrency() first, or use the non-throwing previewDiscount(). A free-shipping coupon’s discountFor() is zero and apply() returns the price unchanged.

Attributes and casts

  • code — stored trimmed and upper-cased, however it’s written.
  • type — the DiscountType enum.
  • value, usage, max_usage, max_usage_per_redeemer — integers.
  • currency — ?Currency (money’s AsCurrency cast).
  • minimum_spend, max_discount — ?Money sharing the currency column.
  • activated_at, expires_at — datetimes.
  • meta — a collection for free-form data such as the campaign a coupon belongs to.

redemptions() is a HasMany relation to CouponRedemption. Deleting a coupon soft-deletes it; a force delete cascades to its redemption rows.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.