---
title: "The Coupons facade — Coupons for Laravel | Roundly"
description: "One entry point for everything — create, look up, check, preview, redeem, revoke, expire and prune coupons, flat or through a Coupons::code() handle."
url: https://roundly-consulting.com/open-source/docs/coupons-for-laravel/coupons-facade
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 Coupons facade

RoundlyConsulting\\Coupons\\Facades\\Coupons is the entry point for everything the package does. Its root is CouponManager, an injectable singleton — see DI and actions to use it without the facade. The full surface:

```php
use RoundlyConsulting\Coupons\Enums\DiscountType;
use RoundlyConsulting\Coupons\Facades\Coupons;

$coupon = Coupons::generate(DiscountType::Percentage, value: 2000, code: 'SUMMER', maxUsage: 100); // 20 %
$coupon = Coupons::generate(DiscountType::Fixed, value: 500, code: 'FIVE', currency: 'EUR');       // 5.00 EUR off
$coupon = Coupons::create($data);         // from a CreateCouponData
$coupon = Coupons::createQuietly($data);  // without dispatching CouponCreated (seeders, fixtures)

// One coupon, by code or model:
Coupons::code('SUMMER')->check($cart, $user);   // ?RedemptionFailureReason — null = redeemable
Coupons::code('SUMMER')->preview($cart);        // Money — the discount, without redeeming
Coupons::code('SUMMER')->redeem($cart, $user);  // RedemptionResult
Coupons::code('SUMMER')->revoke();              // expire it now (reversible), fires CouponRevoked

// The same verbs, flat:
Coupons::check('SUMMER', $cart, $user);
Coupons::preview($coupon, $cart);
Coupons::redeem('SUMMER', $cart, redeemer: $user);
Coupons::revoke($coupon);                 // a Coupon or a code

// Lookups:
Coupons::find('SUMMER');                  // ?Coupon
Coupons::findOrFail('SUMMER');            // throws CouponNotFound
Coupons::exists('SUMMER');                // bool
Coupons::redeemable()->get();             // query builder of coupons redeemable right now

// Maintenance (the console commands call these):
Coupons::expireAll();                     // revoke every live coupon; returns the count
Coupons::expireAll(code: 'SUMMER');       // only the live coupon holding that code
Coupons::prune(days: 30);                 // soft-delete coupons expired 30+ days ago; returns the count
Coupons::prune(days: 30, force: true);    // delete them permanently, previously trashed ones included
```

## Every method

| Method | Returns | Description |
| --- | --- | --- |
| `generate()` | `Coupon` | Quick creation from a type, value, optional code, max usage and currency. value is minor units (Fixed, needs a currency) or basis points (Percentage). Fires CouponCreated. |
| `create()` | `Coupon` | Create from a CreateCouponData DTO; fires CouponCreated. An explicit code a live coupon already holds throws CouponCodeTaken. |
| `createQuietly()` | `Coupon` | The same, without dispatching CouponCreated — for seeders and fixtures. |
| `find()` | `?Coupon` | Look up by code — case-insensitive, surrounding whitespace ignored. |
| `findOrFail()` | `Coupon` | Look up or throw CouponNotFound. |
| `exists()` | `bool` | Whether a coupon with that code exists. |
| `redeemable()` | `Builder<Coupon>` | Query of coupons redeemable right now — active, not expired, not exhausted. |
| `code()` | `CouponCode` | A handle on one coupon, by code or model, with check(), preview(), redeem() and revoke(). |
| `check()` | `?RedemptionFailureReason` | The first reason redemption would refuse the coupon, or null. Takes an optional price and redeemer. Never throws, locks or writes; an unknown code reports NotFound. |
| `preview()` | `Money` | The discount for a price without redeeming — zero on a currency mismatch. Throws CouponNotFound for an unknown code. |
| `redeem()` | `RedemptionResult` | Validate and redeem atomically by code or model, with an optional redeemer. |
| `revoke()` | `Coupon` | Reversibly revoke a coupon or code: expires\_at = now, saved, CouponRevoked fired. Throws CouponNotFound for an unknown code. |
| `expireAll()` | `int` | Revoke every live coupon — or only the live one holding the given code — and return the count. Backs coupons:expire. |
| `prune()` | `int` | Delete coupons that expired at least $days days ago (default 30) — soft by default, permanently with $force — and return the count. Backs coupons:prune; a negative window throws InvalidArgumentException. |
| `fake()` | `CouponsFake` | Swap the manager — for the facade and every injected CouponManager — for a recorder that writes nothing, and return it for assertions. |

## The code() handle

Coupons::code() takes a code or a Coupon model and returns a CouponCode handle. Each of its methods delegates to the flat verb of the same name, so the two forms are interchangeable and the fake records both:

| Method | Returns | Same as |
| --- | --- | --- |
| `check(?Money $price = null, ?Model $redeemer = null)` | `?RedemptionFailureReason` | `Coupons::check($coupon, $price, $redeemer)` |
| `preview(Money $price)` | `Money` | `Coupons::preview($coupon, $price)` |
| `redeem(Money $price, ?Model $redeemer = null)` | `RedemptionResult` | `Coupons::redeem($coupon, $price, $redeemer)` |
| `revoke()` | `Coupon` | `Coupons::revoke($coupon)` |

## Checking before checkout

check() answers “why won’t this code work?” before checkout. It runs the same checks, in the same order, as redeem() and returns the first failing RedemptionFailureReason — NotFound, CurrencyMismatch, MinimumSpendNotMet, Expired, AtMaxUsage or AlreadyRedeemed — or null. Without a price it skips the currency and minimum-spend checks; without a redeemer it skips the per-redeemer cap. translationKey() gives you the same message the validation rule shows:

```php
use RoundlyConsulting\Coupons\Facades\Coupons;

if ($reason = Coupons::code($request->code)->check($cart, $request->user())) {
    return back()->withErrors(['code' => __($reason->translationKey(), ['code' => $request->code])]);
}

$discount = Coupons::code($request->code)->preview($cart); // show it before the order is placed
```

preview() never throws on a currency mismatch — it returns zero — but it does throw CouponNotFound for an unknown code. redeem() and revoke() accept a code or a Coupon. Every lookup matches codes case-insensitively and ignores surrounding whitespace, so summer and SUMMER are the same coupon; a blank code and a soft-deleted Coupon instance are NotFound for check(), just as redeem() throws CouponNotFound for them.

## New coupons start inactive

A new coupon has no activated\_at, so redeeming it throws CouponExpired until you activate it with $coupon->activate()->save() — or pass a future CarbonInterface to schedule it.

## Revoking

revoke() is a reversible kill-switch: it sets expires\_at to now — the row is not deleted and its history stays intact — and fires CouponRevoked. Put the coupon back in circulation by moving or clearing expires\_at:

```php
Coupons::revoke('SAVE20');                        // expires_at = now, fires CouponRevoked

$coupon = Coupons::findOrFail('SAVE20');
$coupon->expire(now()->addMonth())->save();       // back in circulation until next month
$coupon->update(['expires_at' => null]);          // or: no expiry at all
```

expireAll() does the same for every live coupon — or only the live one holding a code — and returns the count. prune() soft-deletes coupons that expired a given number of days ago — their codes are free to issue again — or deletes them permanently with force: true, and refuses a negative window with an InvalidArgumentException, because a window in the future would delete live coupons. Both back the Artisan commands.

createQuietly() creates a coupon without dispatching CouponCreated — useful for seeders and fixtures that shouldn’t trigger your listeners.

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