All events live in RoundlyConsulting\Coupons\Events. CouponCreated, CouponRedeemed, CouponExhausted and CouponRevoked implement Laravel’s ShouldDispatchAfterCommit; CouponRedemptionFailed fires right away:
| Event | When | Payload |
|---|---|---|
CouponCreated | A coupon is created through create() or generate() — not createQuietly(). | Coupon $coupon |
CouponRedeemed | A redemption succeeds. | Coupon $coupon, RedemptionResult $result |
CouponRedemptionFailed | A redemption attempt is rejected — the exception is still thrown. | string $code, RedemptionFailureReason $reason, ?Model $redeemer |
CouponExhausted | A redemption consumes the final available use (fires once). | Coupon $coupon |
CouponRevoked | A coupon is revoked via Coupons::revoke(), Coupons::expireAll() or coupons:expire (once per coupon). | Coupon $coupon |
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Coupons\Events\CouponExhausted;
use RoundlyConsulting\Coupons\Events\CouponRedeemed;
use RoundlyConsulting\Coupons\Events\CouponRedemptionFailed;
Event::listen(function (CouponRedeemed $event): void {
logger()->info('Coupon redeemed', [
'code' => $event->coupon->code,
'discount' => (string) $event->result->discount, // "12.50 EUR"
]);
});
Event::listen(function (CouponRedemptionFailed $event): void {
logger()->warning('Coupon rejected', [
'code' => $event->code,
'reason' => $event->reason->value, // e.g. "expired", "at_max_usage"
]);
});
Event::listen(function (CouponExhausted $event): void {
logger()->info('Coupon exhausted', ['code' => $event->coupon->code]);
});Guarantees
- CouponRedemptionFailed fires once per rejected redemption attempt — never from Coupons::check(), the Redeemable rule or isRedeemableBy() — and the matching exception is still thrown. $event->reason is a RedemptionFailureReason: not_found, currency_mismatch, minimum_spend_not_met, expired, at_max_usage or already_redeemed.
- CouponExhausted fires exactly once — on the redemption that brings usage up to max_usage — never for unlimited coupons, and never again on a later rejected attempt.
- CouponRevoked fires for Coupons::revoke() and once per coupon expired by Coupons::expireAll() or coupons:expire.
- CouponCreated doesn’t fire for createQuietly().
Events and transactions
CouponCreated, CouponRedeemed, CouponExhausted and CouponRevoked wait for the outermost transaction — yours, or the redemption’s own — to commit, and are dropped if it rolls back. A checkout that redeems a coupon and then fails its payment never announces the redemption.
CouponRedemptionFailed fires right away, even inside a transaction: the attempt happened either way. A failed check raises it inside the locked redemption transaction, which rolls back when the exception is thrown, so database writes made by a synchronous CouponRedemptionFailed listener are rolled back with it. An unknown code string is rejected before the transaction opens.
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.