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 usageState checks
| Method | True when / returns |
|---|---|
isActive(): bool | activated_at is set and in the past. |
isExpired(): bool | expires_at is set and in the past. |
canBeApplied(): bool | Active, not expired and not at max usage. |
isAtMaximumUsage(): bool | A positive max_usage has been reached (0 = unlimited). |
isAtMaximumUsageFor(Model $redeemer): bool | A positive max_usage_per_redeemer has been reached for that redeemer. |
usageBy(Model $redeemer): int | Tracked redemptions by that redeemer. |
hasBeenUsedAtLeastOnce(): bool | usage > 0. |
isFreeShipping() / appliesToShipping(): bool | The type is FreeShipping — zero your own shipping line. |
appliesToCurrency(Money $price): bool | Unlocked, or locked to the price’s currency. |
meetsMinimumSpend(Money $price): bool | No 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 DiscountStackRounding 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 JPYdiscountFor() 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 cryptoBy 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.