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

Generated codes

When you don’t pass a code, CreateCouponAction generates one: code.length symbols, each drawn uniformly from code.charset by PHP’s Random\Randomizer, whose default engine is the CSPRNG. Leave out look-alike characters if customers type codes by hand:

// config/coupons.php
'code' => [
    'length' => 8,
    'charset' => 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789', // no 0/O or 1/I look-alikes
],
use RoundlyConsulting\Coupons\Enums\DiscountType;
use RoundlyConsulting\Coupons\Facades\Coupons;

$coupon = Coupons::generate(DiscountType::Percentage, value: 1000); // no code given

$coupon->code; // e.g. "K7QM2XWD"

Uniqueness

A generated code is checked against every existing coupon, soft-deleted ones included — even though a pruned coupon’s code is free to reuse, a fresh code never collides with its redemption history. If 10 candidates in a row are taken, the code space is too small: the action throws InvalidCouponConfiguration telling you to widen it or pass an explicit code, rather than looping forever.

Case-insensitive codes

Codes are case-insensitive. Every code is stored trimmed and upper-cased — through the action, a factory or straight onto the model — so ' summer ' is stored as SUMMER. Every lookup normalises the code the same way: find(), exists(), check(), preview(), redeem(), expireAll(), the validation rule, whereCode(), hasRedeemed() and {coupon} route binding. Matching and uniqueness never depend on your database’s collation:

use RoundlyConsulting\Coupons\Enums\DiscountType;
use RoundlyConsulting\Coupons\Facades\Coupons;
use RoundlyConsulting\Coupons\Models\Coupon;

$coupon = Coupons::generate(DiscountType::Percentage, 1000, code: '  summer ');
$coupon->code;                                   // "SUMMER"

Coupons::find('Summer');                         // the same coupon
Coupons::exists(' SUMMER ');                     // true
Coupon::query()->whereCode('summer')->first();   // the same coupon

Validation

Both keys are validated every time a code is generated, and the message names the key. A charset message never prints the alphabet — it is the key space every generated code is drawn from.

  • code.length — an integer from 4 to 64; an integer env string such as “8” is accepted, while “eight” or “8.5” throws.
  • code.charset — a string of at least 2 symbols, with no duplicates (case-insensitively), whitespace, control characters or invalid UTF-8. It’s upper-cased like every code, so “a” and “A” are one symbol; multibyte symbols are fine.
  • A key that is not set — absent, null or blank, like COUPONS_CODE_LENGTH= — falls back to the default: 6 characters from A–Z and 0–9.
use RoundlyConsulting\Coupons\Enums\DiscountType;
use RoundlyConsulting\Coupons\Exceptions\InvalidCouponConfiguration;
use RoundlyConsulting\Coupons\Facades\Coupons;

config()->set('coupons.code.charset', 'AaBb'); // "a" and "A" are one symbol

try {
    Coupons::generate(DiscountType::Percentage, value: 1000);
} catch (InvalidCouponConfiguration $e) {
    $e->getMessage();
    // "Configuration value [coupons.code.charset] must not contain duplicate symbols (case-insensitively)."
}

Explicit codes are never checked against the format — pass any code you like, such as LAUNCH20. They are still trimmed and upper-cased, and a blank one throws InvalidCouponDefinition.

In the about command

php artisan about --only=coupons reports the format by size only, e.g. “8 chars from a 32-symbol alphabet”. An unusable value shows as INVALID: … instead of failing the command.

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.