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

Money, units & currency lock

Every amount is a RoundlyConsulting\Money\Money from money-for-laravel: exact minor units as an integer string, in a registered currency with its real exponent — no floats anywhere. A coupon’s integer value column means something different per discount type:

Typevalue meansExample
FixedMinor units of the coupon’s currency500 + EUR = 5.00 EUR off
PercentageWhole basis points, 0..10 0002500 = 25 %, 1250 = 12.5 %
FreeShippingIgnored (0)—

Minor units follow each currency’s real exponent, so zero- and three-decimal currencies are exact:

use RoundlyConsulting\Money\Money;

Money::ofMinor(500, 'EUR');    // 5.00 EUR
Money::ofMinor(500, 'JPY');    // 500 JPY   (zero-decimal currency)
Money::ofMinor(1500, 'BHD');   // 1.500 BHD (three-decimal currency)

The currency lock

A coupon with a non-null currency only applies to prices in that currency. A coupon must be locked when it is Fixed, or when it has a minimum_spend or max_discount — both are stored as Money sharing the coupon’s currency column. Percentage and free-shipping coupons without a minimum spend or cap may stay unlocked and apply to any currency; the discount is then computed in the price’s currency.

$coupon->currency;                 // ?Currency — null = any currency, or the lock
$coupon->minimum_spend;            // ?Money    — null = no minimum
$coupon->max_discount;             // ?Money    — null = uncapped

$coupon->appliesToCurrency($price);   // bool — unlocked, or locked to $price's currency
$coupon->meetsMinimumSpend($price);   // bool — no minimum, or $price >= minimum_spend
  • CreateCouponAction enforces the lock: a Fixed coupon without a currency throws InvalidCouponDefinition.
  • A minimum spend or cap in another currency than the lock throws money’s CurrencyMismatch.
  • A percentage is stored as whole basis points (0.01 %): CreateCouponData::percentage('12.5') works, '12.345' throws money’s RoundingNecessary.
  • A Fixed row written around the action without a currency is refused when it’s used — Coupon::discount() throws InvalidCouponDefinition.

Filling a model directly

If you write coupon attributes yourself instead of going through CreateCouponAction, set currency before minimum_spend and max_discount: money’s cast refuses to re-denominate a currency column that already holds a different code. Writing around the action also skips its validation, so prefer CreateCouponData where you can.

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

// currency first: minimum_spend and max_discount share that column.
$coupon = new Coupon([
    'code' => 'SPRING10',
    'type' => DiscountType::Percentage,
    'value' => 1000,                                // 10 %
    'currency' => 'EUR',
    'minimum_spend' => Money::ofMinor(2000, 'EUR'),
]);

$coupon->save();

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.