---
title: "The Coupon model — Coupons for Laravel | Roundly"
description: "Lifecycle mutators, state checks, casts and discount math on the Coupon model — activate, expire, cap, apply and discountFor."
url: https://roundly-consulting.com/open-source/docs/coupons-for-laravel/coupon-model
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Coupons for Laravel](https://roundly-consulting.com/open-source/docs/coupons-for-laravel.md)

# 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).

```php
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

| Method | True when / returns |
| --- | --- |
| `isActive(): bool` | activated\_at is set and <= now, like the active() scope. |
| `isExpired(): bool` | expires\_at is set and <= now, like the expired() scope — a coupon revoked this instant is already expired. |
| `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; always false while coupons.redeemer.track is off. |
| `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:

```php
$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:

```php
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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/coupons-for-laravel.md)
