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
| Model | Attribute | Cast / column |
|---|---|---|
| ProductVariant, CartItem, Orders\Item | price | AsMoney::currencyColumn('currency') — decimal(38,0) + currency code |
| Order | currency | AsCurrency — snapshotted on insert |
| Order | discount, store_credit_applied | AsMoney::currencyColumn('currency') — nullable |
| Cart | currency | AsCurrency |
| Shop | currency | raw 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 unchangedOrder 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 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.