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

Money & currencies

Every amount in shops is a money-for-laravel Money: minor units as an exact integer string in a registered currency with its real exponent (JPY 0, EUR 2, BHD 3). Shops ships no money primitive of its own — coupons and credits speak the same type, so nothing is converted between packages:

use RoundlyConsulting\Money\Money;

$price = Money::ofMinor(1000, 'EUR');                      // 10.00 EUR
$same  = Money::ofMajor('10.00', 'EUR');                   // from a decimal string
$total = Money::sum([$price, Money::ofMinor(500, 'EUR')]); // 15.00 EUR

$price->minor();              // "1000" (string — exact past int64)
$price->currency()->code;     // "EUR"
(string) $price;              // "10.00 EUR"

Casts and columns

ModelAttributeCast / column
ProductVariant, CartItem, Orders\ItempriceAsMoney::currencyColumn('currency') — decimal(38,0) + currency code
OrdercurrencyAsCurrency — snapshotted on insert
Orderdiscount, store_credit_appliedAsMoney::currencyColumn('currency') — nullable
CartcurrencyAsCurrency
Shopcurrencyraw nullable ISO code — Shop::currency() resolves it
  • The price cast writes the row’s currency column from the Money. It refuses to re-denominate a column that already holds another code, so set currency before price in fill() or update() arrays when changing a variant’s currency.
  • decimal(38,0) is exact to 38 digits on pgsql and MySQL — orderBy, sum and where compare numerically. On SQLite the cast refuses amounts beyond int64.
  • tax_rates.rate stays an unsigned integer of basis points, not money.
// decimal(38,0) compares numerically on pgsql/MySQL — bind minor() strings, never (int):
ProductVariant::query()
    ->where('price', '>=', $minimum->minor())
    ->orderBy('price')
    ->get();

Currency safety

Mixing currencies throws money’s CurrencyMismatch — including adding a variant to a cart or order in another currency:

$cart = Cart::create(['currency' => 'EUR']);
Shops::cart($cart)->add($usdVariant);
// throws RoundlyConsulting\Money\Exceptions\CurrencyMismatch — the cart is unchanged

Order currency snapshot

An order stores its own currency, set on insert when none was given: the cart’s at place-order, else its shop’s (its shop_id or the bound CurrentShop), else pricing.default_currency. Pricing and AddOrderItemAction use the order’s currency, never the config — changing SHOPS_DEFAULT_CURRENCY does not re-denominate placed orders.

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.