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

Artisan commands

credits:modify applies one credit change to every entity resolved by the credits.modifiable config, through Credits::for($entity) — so Credits::fake() records it. Register one or more resolver closures — each receives a $modify callback to call per entity:

// config/credits.php
use App\Models\User;

'modifiable' => [
    function (Closure $modify): void {
        User::query()->each(fn (User $user) => $modify($user));
    },
],

Then run it:

php artisan credits:modify --amount=10 --description="monthly bonus"
# Modified credits on 42 entities.

php artisan credits:modify --amount=50 --description="monthly bonus" --bucket=promotional
php artisan credits:modify --amount=-5 --description="correction" --allow-overdraft
OptionDefaultDescription
--amount—Required: the credit amount to apply, a non-zero integer — negative to deduct. A missing, non-integer or 0 value prints an error and exits 1 before any resolver runs, so nothing is written.
--descriptionnullAn optional human-readable description stored on each row.
--bucketconfigured defaultThe named bucket to apply the change to. Omit to use default_bucket.
--allow-overdraftfalsePermit deductions that drive the balance below minimum_balance (which may be other than zero).
  • Each row the command writes carries meta ['info' => 'Credits modified by credits:modify command.'].
  • Resolved entities that are not Creditable models are skipped with a warning rather than failing the run.
  • An entity whose deduction the overdraft guard refuses is skipped too: the run carries on with the rest and prints a Refused line for each.
  • Once every entity has been visited, the command reports how many were refused and exits non-zero.
  • Each change is its own transaction, so the entities that were charged stay charged — re-run only for the refused ones (for example with a resolver that selects them), not the whole set.
  • With no resolvers registered, the command succeeds and changes nothing.
php artisan credits:modify --amount=-50 --description="monthly fee"
# Refused App\Models\User #2: Insufficient credits: tried to deduct 50 but only 20 are available.
# Modified credits on 41 entities.
# Refused 1 entity with insufficient credits.
echo $?   # 1 — the run finished, but not every entity was charged

credits:reset

Resets buckets to a balance at most once per period for every owner with rows in them, or for the credits.modifiable owners with --resolvers — meant for the scheduler. Each owner goes through Credits::for($owner), so the fake records it:

Argument / optionDefaultMeaning
bucket*—One or more buckets to reset.
--periodmonthlydaily, weekly, monthly or yearly.
--key—An explicit period key instead of --period (e.g. 2026-11).
--to0The balance to reset each bucket to; must be an integer.
--timezoneapp.timezoneThe timezone the period is read in.
--descriptionnullDescription for the delta rows.
--allow-overdraftfalseReset even when held credits would take the available balance below the minimum.
--resolversfalseReset the owners the credits.modifiable resolvers yield instead of every owner with rows in the bucket.

See Periodic resets for scheduling examples, the period keys and what a refused owner does.

credits:expire-holds

Marks holds past their expiry as expired and fires CreditHoldExpired for each — the same sweep as Credits::expireHolds(). Schedule it every minute when holds are on:

php artisan credits:expire-holds
# Expired 3 holds.

php artisan credits:expire-holds --connection=tenant

Inspecting the configuration

The package contributes a section to Laravel’s about command:

php artisan about --only=credits
RowShows
ModelThe resolved ledger model’s class name, or INVALID when credits.model is not Credit or a subclass.
Primary key typecredits.primary_key_type, or INVALID when it is not bigint, uuid or ulid — surfaced because a non-bigint id cannot be held by another package’s morphs() column on a strict engine.
Creditable key typecredits.key_type, or INVALID when it is not bigint, uuid or ulid.
OverdraftALLOWED or BLOCKED — read from allow_overdraft as a boolean, the same way the guard reads it; INVALID when the value is not a boolean.
Minimum balancecredits.minimum_balance, or INVALID when the value is malformed — about keeps working.
Default bucketcredits.default_bucket, or INVALID when the value is malformed.
Scalecredits.scale, or INVALID when the value is malformed.
RoundingThe display rounding mode in its config spelling, or INVALID when credits.rounding names no mode.
Modifiable resolversNONE or “N registered” — a count, never what they resolve; INVALID when credits.modifiable is malformed.
HoldsENABLED or DISABLED — credits.holds.enabled read as a boolean; INVALID when it is not one.
Hold expiryThe default hold lifetime (“30 min”), or NEVER when credits.holds.expire_after_minutes is not set; INVALID when malformed.

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.