Coupons & discounts
Coupons are powered entirely by coupons-for-laravel — shops stores no coupons of its own. Create coupons with that package, then reference them by code:
use RoundlyConsulting\Coupons\DataTransferObjects\CreateCouponData;
use RoundlyConsulting\Coupons\Enums\DiscountType;
use RoundlyConsulting\Coupons\Facades\Coupons;
use RoundlyConsulting\Money\Money;
// Percentage values are basis points: 1000 = 10 %
Coupons::generate(DiscountType::Percentage, 1000, 'WELCOME10')->activate()->save();
// Or build them with the coupons DTO:
Coupons::create(CreateCouponData::fixed(Money::ofMinor(500, 'EUR'), 'FIVEOFF'))->activate()->save(); // locked to EUR
Coupons::create(CreateCouponData::freeShipping('SHIPFREE'))->activate()->save();Percentages are basis points. Fixed coupons, and any coupon with a minimum spend or cap, are locked to one currency — against a cart or order in another currency they resolve to no discount. New coupons are inactive until activated.
Previewing a code
Shops::coupons()->preview() resolves a code through the bound DiscountResolver — no redemption, no buyer (per-customer limits are only enforced at place-order). An unknown or currently non-redeemable code previews as a zero discount:
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Facades\Shops;
$result = Shops::coupons()->preview('WELCOME10', Money::ofMinor(1000, 'EUR'));
$result->discount; // Money — 1.00 EUR off
$result->freeShipping; // bool
$result->code; // "WELCOME10"
$result->found; // a coupon with that code exists
$result->hasDiscount(); // the discount is positive
$result->source; // the coupon as a money Discount, when it applies
if ($result->found && ! $result->hasDiscount()) {
// exists but not applicable now: inactive, expired, used up, below minimum spend, other currency
}Cart prices
A cart prices with the code you pass, else its stored coupon_code:
$cart->update(['coupon_code' => 'WELCOME10']);
Shops::cart($cart)->price()->getFinalPrice(); // priced with the stored code
Shops::cart($cart)->price('SHIPFREE')->shippingCost(); // preview another code — nothing is redeemedAt place-order
The coupon is looked up, checked for the buyer and subtotal, its discount resolved, then redeemed once for the buyer under the coupon’s row lock (usage increments, per-customer caps apply, the coupons events fire). The discount is snapshotted onto the order and the coupon linked. Expiring, revoking or exhausting the coupon afterwards — including the single use this order consumed — never changes what the order costs; free-shipping coupons zero the shipping line:
$order->coupon; // ?Coupon — the shops.discounts.coupon_model
$order->discount; // ?Money — granted at placement, in the order currency
$order->free_shipping; // bool
$order->coupon_code; // ?stringA missing or non-redeemable code is silently skipped — including one the locked redemption refuses after a racing checkout took its last use.
Combining discounts
DiscountResult::source is the coupon as a money Discount, so you can stack it with your own discounts:
use RoundlyConsulting\Money\Discounts\DiscountStack;
$breakdown = DiscountStack::of($result->source, $loyaltyDiscount)->apply($subtotal, $shipping);A custom resolver
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Contracts\DiscountResolver;
use RoundlyConsulting\Shops\Discounts\DiscountResult;
final class StaffDiscountResolver implements DiscountResolver
{
public function resolve(string $code, Money $goods): DiscountResult
{
return $code === 'STAFF'
? new DiscountResult($goods->percentage(20), code: $code, found: true)
: DiscountResult::none($goods->currency(), $code);
}
}
// config/shops.php → 'discounts' => ['resolver' => StaffDiscountResolver::class, ...]A custom resolver changes previews, cart prices and the discount snapshotted at place-order. Checkout still links and redeems coupons through coupons-for-laravel, so a code must exist there to be applied to an order. Swap the coupon model with discounts.coupon_model (a subclass of the coupons Coupon).
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.