Granting & deducting
Every change appends one ledger row. Grant with add(), deduct with deduct() — both take a positive amount — or pass a signed amount to modify(); the optional description and meta array are stored on the row:
use RoundlyConsulting\Credits\Facades\Credits;
Credits::for($user)->add(100, 'signup bonus'); // a grant — returns the Credit row
Credits::for($user)->deduct(30, 'purchase', ['order_id' => 42]); // a deduction, passed as a positive amount
Credits::for($user)->balance(); // 70
Credits::for($user)->modify(-5, 'correction'); // a signed changeOn a model with the HasCredits trait, modifyCredits() is the shorthand — a positive amount grants, a negative one deducts:
$user->modifyCredits(100, 'signup bonus');
$user->modifyCredits(-30, 'purchase', ['order_id' => 42]);
$user->creditsBalance(); // 70It returns the recorded Credit row:
$credit = $user->modifyCredits(25, 'referral reward', ['referrer_id' => 7]);
$credit->amount; // 25
$credit->description; // 'referral reward'
$credit->meta; // ['referrer_id' => 7]
$credit->bucket; // 'default'
$credit->created_at; // when the change was recordedNamed arguments keep longer calls readable — the full signature is modifyCredits(int $amount, ?string $description = null, ?array $meta = null, bool $allowOverdraft = false, ?string $bucket = null):
$user->modifyCredits(
amount: -30,
description: 'order #42',
meta: ['order_id' => 42],
allowOverdraft: false,
bucket: 'purchased',
);Setting an exact balance
setTo() computes the delta from the current balance and records a single adjusting row. When the balance already matches, it records nothing, returns null and fires no event. It takes the owner lock and reads the balance under a row lock, in one transaction with the write, so two racing calls serialise — the second computes its delta from the balance the first left behind:
Credits::for($user)->setTo(500, 'manual adjustment'); // one delta row; null when already there
Credits::for($user)->bucket('promotional')->setTo(200); // per bucket, like every other callThe trait form is setCreditsTo():
$user->creditsBalance(); // 70
$user->setCreditsTo(500, 'manual adjustment'); // records +430, returns the Credit row
$user->setCreditsTo(500); // already 500: records nothing, returns null
$user->setCreditsTo(200, bucket: 'promotional'); // per bucket, like every other methodA downward adjustment is recorded as a deduction, so the overdraft guard applies to it — setting a balance below the floor needs allowOverdraft: true.
An append-only history
Nothing is ever overwritten: amounts are signed integers, each change is its own time-stamped row, and the balance is their sum. To reverse a change, record a compensating row — the history then shows both the change and its correction.
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.