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

RoundlyConsulting\Credits\Events\CreditsModified is dispatched after every recorded change. It carries the creditable, the new Credit row and the resulting balance:

use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Credits\Events\CreditsModified;

Event::listen(function (CreditsModified $event): void {
    // $event->creditable, $event->credit, $event->balance
});

Payload

final class CreditsModified implements ShouldDispatchAfterCommit
{
    use Dispatchable;

    public function __construct(
        public readonly Model&Creditable $creditable,
        public readonly Credit $credit,
        public readonly int $balance,   // the bucket's balance this change produced
    ) {}
}

balance is the balance of the bucket that changed — read it from $event->credit->bucket — not a total across buckets. It is the balance this change produced, read under the owner lock inside the change’s transaction, so a racing change never leaks into it.

When it fires

  • After every successful write — add(), deduct(), modify(), setTo() and their Money forms on a scope, the matching trait methods — and for each entity credits:modify changes.
  • Only once the change is committed: CreditsModified implements ShouldDispatchAfterCommit, so inside setTo() or your own DB::transaction() listeners receive it when the outermost transaction commits — and never for a change that is rolled back. A listener that throws cannot roll the change back; check before making the change instead.
  • Not when setCreditsTo() is a no-op because the balance already matches.
  • Not when the overdraft guard, the 64-bit bound or a money check refuses the change — nothing was written.

A listener

Send a receipt, update a dashboard or alert on a low balance:

use Illuminate\Support\Facades\Log;

Event::listen(function (CreditsModified $event): void {
    Log::info("Balance for {$event->creditable->getKey()} is now {$event->balance}");

    if ($event->credit->amount < 0 && $event->balance < 10) {
        // $event->credit->bucket is running low — notify the owner
    }
});

Hold events

Holds fire their own events, all ShouldDispatchAfterCommit — heard only once the transaction commits. A reset fires CreditsModified for its delta row (none when the balance already matched):

EventPropertiesWhen
CreditsHeldcreditable, hold, availableA hold was created; available is the bucket’s available balance after it.
CreditHoldCapturedcreditable, hold, credit, releasedA hold was captured; credit is the debit, released the part freed (0 for a full capture). CreditsModified fires for the debit too.
CreditHoldReleasedholdA hold was released.
CreditHoldExpiredholdThe sweep expired a hold; the owner via $hold->creditable, which may be gone.
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Credits\Events\CreditHoldCaptured;
use RoundlyConsulting\Credits\Events\CreditHoldExpired;

Event::listen(function (CreditHoldCaptured $event): void {
    // $event->creditable, $event->hold, $event->credit (the debit), $event->released (0 for a full capture)
});

Event::listen(function (CreditHoldExpired $event): void {
    // $event->hold — the owner via $event->hold->creditable, which may be gone
});

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.