Periodic resets
A periodic reset sets chosen buckets to an exact balance once per period — monthly free credits, a daily allowance. It is idempotent per owner, bucket and period, so a double-scheduled run, two servers or a retried job never reset twice. Resets need the credit_resets table (see Installation) and no switch:
use RoundlyConsulting\Credits\Enums\ResetPeriod;
use RoundlyConsulting\Credits\Facades\Credits;
$result = Credits::for($user)->bucket('free')->reset(ResetPeriod::Monthly, to: 100);
$result->reset; // true — false when this bucket was already reset this period
$result->period; // '2026-11'
$result->credit; // the delta row, or null (already reset, or the balance already matched)
Credits::reset($user, 'billing-cycle-7', to: 500, bucket: 'pro'); // an explicit period key
$user->resetCredits(ResetPeriod::Daily, to: 10, bucket: 'daily_allowance');- Idempotent per period: one credit_resets row per owner, bucket and period key, written in the reset’s transaction under the owner lock. A second reset in the same period writes nothing and returns reset: false.
- Period keys are read from now, in the app timezone: Daily → 2026-11-09, Weekly → 2026-W46 (ISO week-year), Monthly → 2026-11, Yearly → 2026. Or pass your own key (1–64 characters, trimmed) for billing cycles that do not follow the calendar.
- The reset is a setTo($to): one delta row (meta ['reset_period' => key] unless you pass meta), CreditsModified, and the same guard — lowering a bucket below its held credits is refused unless an overdraft is allowed. A refused reset leaves no marker, so the next run retries it.
- Also Credits::reset($owner, $period, to:, bucket:, description:, meta:, allowOverdraft:) and $user->resetCredits($period, to:, …, bucket:).
Scheduling — credits:reset
credits:reset {bucket*}
{--period=monthly : daily, weekly, monthly or yearly}
{--key= : an explicit period key instead of --period}
{--to=0 : the balance to reset to}
{--timezone= : the timezone the period is read in (default app.timezone)}
{--description= : description for the delta rows}
{--allow-overdraft : reset even when held credits would breach the minimum}
{--resolvers : reset the owners the credits.modifiable resolvers yield}| Argument / option | Default | Meaning |
|---|---|---|
bucket* | — | One or more buckets to reset. |
--period | monthly | daily, weekly, monthly or yearly. |
--key | — | An explicit period key instead of --period (e.g. 2026-11). |
--to | 0 | The balance to reset each bucket to; must be an integer. |
--timezone | app.timezone | The timezone the period is read in. |
--description | null | Description for the delta rows. |
--allow-overdraft | false | Reset even when held credits would take the available balance below the minimum. |
--resolvers | false | Reset the owners the credits.modifiable resolvers yield instead of every owner with rows in the bucket. |
By default it resets every owner that has ledger rows in each bucket — owners are loaded through their morph type, and a row whose owner is gone is reported and skipped. Use --resolvers to reset the owners your credits.modifiable closures yield instead, which you need for owners that have never had credits. The period key is read once per run, so a run that straddles midnight resets everyone into the same period:
// routes/console.php — free credits back to 100 on the 1st, in the business's timezone
Schedule::command('credits:reset free --period=monthly --to=100 --timezone=Europe/Bratislava')
->monthlyOn(1, '00:05')
->timezone('Europe/Bratislava');
// daily allowance, topping up every user (resolvers in credits.modifiable)
Schedule::command('credits:reset daily_allowance --period=daily --to=10 --resolvers')->dailyAt('00:01');Running the same command twice in one period resets nothing the second time. It prints how many owners it reset and how many were already reset in the period; an owner the guard refuses is reported, the run carries on and the command exits 1. Bad options — --period=hourly, a non-integer --to, an oversized --key, an unknown --timezone — fail before anything is written.
The credit_resets table
| Column | Type | Notes |
|---|---|---|
creditable_type / creditable_id | morph pair, id per key_type | |
bucket | string | |
period | string(64) | The period key. |
reset_to | bigInteger | |
credit_id | per primary_key_type, nullable | The delta row, when one was written. |
created_at | timestamp |
Primary key (creditable_type, creditable_id, bucket, period) — the idempotency guarantee. It is never read for a balance.
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.