L LAB

The CouponService API

CouponService is the only entry point your application needs. It is bound as a singleton, so resolve it from the container:

use Mrsuner\Coupon\Services\CouponService;

$coupons = app(CouponService::class);
// or inject CouponService into a constructor / controller method

generate()

Creates and persists a single coupon code. The code is auto-generated when omitted (see generation config).

$coupon = $coupons->generate([
    'type'  => 'percent_off',          // free-form string; the package stores, never interprets it
    'value' => ['percent' => 20],      // effect payload
    'name'  => 'Launch promotion',     // optional admin label
    'restrictions' => [                // all optional
        'max_uses'   => 500,           // total redemptions across all users
        'per_user'   => 1,             // redemptions per unique redeemable
        'expires_at' => '2026-12-31T23:59:59Z',
        'min_amount' => 1000,          // minimum transaction value, in cents
    ],
]);

Throws DuplicateCouponCodeException (an InvalidArgumentException) when a supplied custom code already exists.

generateBulk()

Generates many unique codes sharing the same type, value and restrictions. When provided, code is treated as a prefix (LAUNCHLAUNCH-A3F9K2).

$codes = $coupons->generateBulk(50, [
    'code'  => 'LAUNCH',
    'type'  => 'free_months',
    'value' => ['months' => 1],
    'restrictions' => ['per_user' => 1],
]);

// returns Collection<CouponCode>

validate()

Checks whether a code can be redeemed without recording anything. Returns a ValidationResult value object.

$result = $coupons->validate('LAUNCH20', $user);

if (! $result->valid) {
    // $result->error is one of the constants below
    // $result->coupon is the matched coupon (null when not found)
}

ValidationResult error constants:

ConstantValueMeaning
NOT_FOUNDnot_foundNo code matches
INACTIVEinactiveis_active is false
EXPIREDexpiredPast restrictions.expires_at
EXHAUSTEDexhaustedtimes_redeemed >= max_uses
USER_LIMITuser_limit_reachedThe redeemable hit its per_user limit

The error strings double as translation keys, e.g. __('coupon.'.$result->error).

redeem()

Validates, records the redemption and fires CouponRedeemed — atomically.

$redemption = $coupons->redeem('LAUNCH20', $user, [
    'order_id' => $order->id,   // arbitrary context, stored on the redemption
]);

Internally redeem():

  1. Calls validate($code, $redeemable) and throws CouponNotRedeemableException if it fails.
  2. In a database transaction (with a row lock to prevent overshooting max_uses): creates the CouponRedemption with a snapshot of the coupon’s type + value, then increments times_redeemed.
  3. Fires new CouponRedeemed($coupon, $redeemable, $redemption) after the transaction commits.

If the transaction rolls back, no event is fired and no counter moves.

Exceptions

Mrsuner\Coupon\Exceptions\
├── CouponNotRedeemableException   — thrown by redeem() on invalid code
│     ->getValidationResult(): ValidationResult
│     ->getMessage(): string       — one of the ValidationResult::* constants
└── DuplicateCouponCodeException   — thrown by generate() on code collision

Map them in bootstrap/app.php for custom HTTP responses:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (CouponNotRedeemableException $e) {
        return response()->json(['message' => __('coupon.'.$e->getMessage())], 422);
    });
})

Next: wire a redeem endpoint and react to redemptions.