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

Money & prices

The price attribute on Purchase, PurchaseItem, Subscription, SubscriptionItem and PurchaseRefund is a money-for-laravel Money, cast with AsMoney over two columns: price (decimal(38,0) minor units) and price_currency (ISO code). Amounts are arbitrary-precision strings — no 32- or 64-bit ceiling on PostgreSQL or MySQL — and currencies such as JPY and BHD use their real exponents:

use RoundlyConsulting\Money\Money;

$purchase->price = Money::ofMinor(2599, 'USD');   // writes price + price_currency
$purchase->price->minor();                         // "2599" (string)
$purchase->price->currency()->code;                // "USD"
$purchase->price->toDecimal();                     // "25.99"
$purchase->price->add(Money::ofMinor(100, 'USD')); // 26.99 USD
$purchase->price->format('en_US');                 // "$25.99"

$purchase->price = null;                           // clears the amount, keeps price_currency

The cast is strict: assigning a raw integer throws InvalidMoneyValue (a bare number has no currency), and an amount stored with a null price_currency throws on read. Factories and seeders pass Money::ofMinor(…).

Store prices

ProviderResult::price() is converted from each store’s wire format without floating-point arithmetic:

StoreSourceConversion
Stripeamount, amount_total, amount_paid, amount_due or amount_refunded, plus currencyMinor units via Money::ofMinor(); ISK, UGX and MGA re-scaled per StripeAmount::SCALE_EXCEPTIONS.
AppleSigned transaction price in milli-units, plus currencyMoney::ofScaled() at scale 3, rounded half away from zero, once; a refund records only the refunded share.
Google subscriptionsLine items’ recurringPrice (units + nanos)Summed exactly via GoogleMoney; prepaid plans carry none.
Google one-time products—The Play Developer API carries no price, so price() is null.

A price the package cannot convert exactly — a float or fractional Stripe amount, an unknown currency, an out-of-range value — becomes null rather than being truncated, and the rest of the result is still recorded.

Totals in SQL

Because price is a numeric column, orderBy('price') and sum('price') work per currency. sum() returns a string on PostgreSQL and MySQL — pass it straight back into Money::ofMinor():

use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Purchases\Models\Purchase;

$sum = Purchase::query()
    ->where('price_currency', 'USD')
    ->sum('price');                    // a string on PostgreSQL and MySQL

$total = Money::ofMinor($sum, 'USD');
$total->format('en_US');

Audit snapshots

Audit notifications store the price as Money::toArray(), with minor as a JSON string so amounts beyond int64 survive. purchases:replay rebuilds it with Money::fromArray() and skips a snapshot whose price cannot be rebuilt:

{ "minor": "1999", "decimal": "19.99", "currency": "USD" }

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.