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

Currency-denominated buckets

A bucket can be denominated in a money-for-laravel currency. Its integers are then minor units of that currency — cents for EUR, whole points for a custom PTS — and it gains money-typed helpers that refuse any other currency. Map bucket names to currency codes:

// config/credits.php
'currencies' => [
    'store_credit' => 'EUR',
    'points' => 'PTS',   // a custom currency — see Custom currencies
],

Then read and write the bucket as Money through its scope:

use RoundlyConsulting\Credits\Facades\Credits;
use RoundlyConsulting\Money\Money;

$store = Credits::for($user)->bucket('store_credit');

$store->addMoney(Money::ofMajor('25.00', 'EUR'), 'Gift card');
$store->deductMoney(Money::ofMinor(1050, 'EUR'), 'Order #42');
$store->modifyMoney(Money::ofMinor(-100, 'EUR'), 'Fee');   // a signed change

$store->money();                  // Money EUR 13.50
$store->currency();               // Currency EUR — null for a plain bucket
$store->format();                 // "13.50" — read at the EUR exponent
$store->formatMoney('en');        // money's locale formatter
$store->balance();                // 1350 — the int API is unchanged

Credits::currency('store_credit');   // Currency EUR — owner-free

The trait has the same helpers on the model:

use RoundlyConsulting\Money\Money;

$user->creditsCurrency('store_credit');   // Currency EUR   (null for a plain bucket)

$user->modifyCreditsMoney(Money::ofMajor('25.00', 'EUR'), 'gift card', bucket: 'store_credit');
$user->modifyCreditsMoney(Money::ofMinor(-1050, 'EUR'), 'order #42', bucket: 'store_credit');

$user->creditsBalanceMoney('store_credit');              // Money EUR 14.50
$user->creditsBalanceMoney('store_credit')->minor();     // "1450" — money amounts are strings
$user->creditsBalanceMoney('store_credit')->toDecimal(); // "14.50"
$user->creditsBalance(bucket: 'store_credit');           // 1450 — the int API is unchanged
$user->displayCreditsBalance('store_credit');            // "14.50" (read at the EUR exponent)
$user->formatCreditsBalance('store_credit', 'en');       // money's locale formatter (symbol + amount)

The money-typed trait helpers

MethodReturnsThrows
creditsCurrency(?string $bucket = null)?Currency — null when the bucket is not listed (or mapped to a blank code)UnknownCurrency (code not registered), InvalidMoneyConfiguration (malformed map)
creditsBalanceMoney(?string $bucket = null, ?CarbonInterface $at = null)Money of the bucket currency — Money::ofMinor(creditsBalance($at, $bucket), $currency)BucketNotDenominatedException
modifyCreditsMoney(Money $amount, ?string $description = null, ?array $meta = null, bool $allowOverdraft = false, ?string $bucket = null)The new Credit rowBucketNotDenominatedException, CurrencyMismatch, AmountOverflow, InsufficientCreditsException
formatCreditsBalance(?string $bucket = null, ?string $locale = null)Money::format($locale) of the balanceBucketNotDenominatedException

How it behaves

  • One write path. addMoney(), deductMoney(), modifyMoney() and modifyCreditsMoney() check the currency, convert with Money::minorInt() and write through the integer path — the same overdraft guard, minimum balance, row lock and CreditsModified event. Every refusal happens before anything is written.
  • Currency identity is code + exponent (money’s Currency::equals()): a USD amount into an EUR bucket throws CurrencyMismatch.
  • displayCreditsBalance() reads a denominated bucket at its currency exponent and renders there unless scale overrides it. displayCredits() takes no bucket, so it always uses credits.scale.
  • The default bucket is denominated too when credits.currencies lists its name — the helpers then work without a bucket argument.
  • Plain buckets keep working side by side; the int API (creditsBalance(), modifyCredits()) is unchanged for denominated buckets.
use RoundlyConsulting\Money\Exceptions\CurrencyMismatch;

try {
    $user->modifyCreditsMoney(Money::ofMajor('5.00', 'USD'), 'refund', bucket: 'store_credit');
} catch (CurrencyMismatch $e) {
    // a USD amount into an EUR bucket — nothing was written
}

$user->creditsBalanceMoney('promotional'); // throws BucketNotDenominatedException (a plain bucket)

Past balances and display scale

creditsBalanceMoney() and displayCreditsBalance() accept a point in time, and the display scale and rounding overrides work from the currency exponent:

$user->creditsBalanceMoney('store_credit', now()->subMonth());          // Money at a past moment
$user->displayCreditsBalance('store_credit', scale: 0);                  // "15" — rounded at the display scale
$user->displayCreditsBalance('store_credit', scale: 4);                  // "14.5000"

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.