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

The Credits facade

RoundlyConsulting\Credits\Facades\Credits (also auto-registered as the global alias Credits) is the recommended entry point. Credits::for($owner) returns an immutable scope: bucket() and allowOverdraft() each return a new scope, so you can keep one and reuse it:

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

Credits::for($user)->add(100, 'Welcome');                       // Credit row
Credits::for($user)->deduct(30, 'Order #42', ['order_id' => 42]); // InsufficientCreditsException below the floor
Credits::for($user)->modify(-5);                                 // a signed change
Credits::for($user)->allowOverdraft()->deduct(500);              // may go below the floor
Credits::for($user)->setTo(0, 'Reset');                          // one delta row; null when already there

Credits::for($user)->balance();                                  // int — the default bucket
Credits::for($user)->balance(now()->subWeek());                  // as of a point in time
Credits::for($user)->has(50);                                    // bool
Credits::for($user)->total();                                    // int — every bucket

$points = Credits::for($user)->bucket('points');
$points->add(100, 'Welcome');
$points->balance();                                              // 100

Credits::for($user)->buckets(['promotional', 'purchased'])->balance(); // summed, read-only
Credits::for($user)->buckets(['promotional', 'purchased'])->has(50);

// A currency-denominated bucket reads and writes 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->money();                                                 // Money EUR 14.50
$store->currency();                                              // Currency EUR
$store->format();                                                // "14.50"
$store->formatMoney('en');                                       // money's locale formatter (symbol + amount)

// Owner-free helpers:
Credits::format(1250, scale: 2);                                 // "1250.00" at credits.scale = 0
Credits::currency('store_credit');                               // ?Currency

add() and deduct() take a non-negative amount and throw InvalidArgumentException otherwise — use modify() for a signed change. The same holds for addMoney(), deductMoney() and modifyMoney(). buckets([...]) only reads, because every write names exactly one bucket.

Scope methods

MethodReturnsWhat it does
bucket($name) / allowOverdraft($allow = true)CreditsScopeA new scope in that bucket / allowed below the floor.
buckets($names)CreditBucketsA read-only sum of several buckets — balance($at) and has($amount, $at).
add($amount, $description, $meta)CreditGrant a non-negative amount.
deduct($amount, $description, $meta)CreditTake a non-negative amount off; guarded by the floor.
modify($amount, $description, $meta)CreditA signed change — positive grants, negative deducts.
setTo($amount, $description, $meta)?CreditOne delta row to an exact balance; null when it already matches.
balance($at) / has($amount = 1, $at)int / boolThe scoped bucket’s balance, optionally as of a point in time.
total($at)intEvery bucket of the owner, whichever bucket is scoped.
currency()?CurrencyThe bucket’s currency, or null for plain credits.
money($at)MoneyA denominated bucket’s balance as Money.
addMoney() / deductMoney() / modifyMoney()CreditThe Money forms of add(), deduct() and modify().
format($scale, $rounding, $at)stringThe balance as a locale-free decimal string (a denominated bucket at its exponent).
formatMoney($locale)stringA denominated bucket’s balance through money’s locale formatter.

Flat methods

The facade also exposes the flat verbs every scope ends in — handy for jobs that already hold a CreditChangeData:

Facade / manager methodReturnsActionNotes
for($owner)CreditsScope—The entry point for one owner.
format($amount, $scale, $rounding, $storedScale)stringFormatCreditsActionNeeds no model.
currency($bucket)?CurrencyResolveBucketCurrencyActionnull = plain credits.
balance($owner, $bucket, $at)intGetCreditsBalanceActionOne bucket.
total($owner, $buckets, $at)intGetCreditsTotalActionnull = every bucket; the list is de-duplicated; [] is 0.
modify($owner, CreditChangeData $data)CreditModifyCreditsActionSigned; overdraft-guarded; fires CreditsModified.
setTo($owner, $amount, $description, $meta, $allowOverdraft, $bucket)?CreditSetCreditsActionOne delta row; null when it already matches.
fake()CreditsFake—Static, on the facade — see Testing.

The HasCredits model methods ($user->modifyCredits(), $user->creditsBalance(), …) are shorthand for the same calls — each goes through Credits::for($user), so they behave exactly like the facade and the fake records them.

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.