---
title: "Coupons & discounts — Shops for Laravel | Roundly"
description: "Coupons from coupons-for-laravel — preview codes, price carts, redeem once at place-order with a locked-in discount, or plug in your own resolver."
url: https://roundly-consulting.com/open-source/docs/shops-for-laravel/coupons
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Shops for Laravel](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)

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

```php
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:

```php
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:

```php
$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 redeemed
```

## At 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:

```php
$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;     // ?string
```

A 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:

```php
use RoundlyConsulting\Money\Discounts\DiscountStack;

$breakdown = DiscountStack::of($result->source, $loyaltyDiscount)->apply($subtotal, $shipping);
```

## A custom resolver

```php
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 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/shops-for-laravel.md)
