tourze/credit-bundle
tourze/credit-bundle provides a reusable credit and points ledger for PHP applications, with account balances, transaction records, and service APIs for awarding, deducting, and querying credits. Integrate it to add auditable credit management.
The package is installed via Composer and follows the standard bundle/package conventions:
composer require tourze/credit-bundle
Because this is a 1.0.x release, treat the surface as stable but small — start by reading, in this order:
credit/balance column versus a transactions/ledger table; that single detail determines almost every downstream pattern.CreditManager, CreditService, or a facade) is what you will actually call from controllers and jobs.Publish whatever is publishable before writing code:
php artisan vendor:publish --provider="Tourze\CreditBundle\CreditServiceProvider"
php artisan migrate
First use case: give a user points and read them back. Wire the service into a controller, grant a fixed amount, redirect, and render the balance on a dashboard. Once that round trip works, add a spend operation and a "insufficient balance" failure path — those two flows cover roughly 80% of what a credits system does in a real app.
If the package is Symfony-flavored under the hood (Tourze publishes many *-bundle packages), verify early that the service provider is genuinely registered with Laravel's container and not just a Symfony-style bundle class. A five-minute php artisan tinker session — resolve the service, call a method, dump the result — will save you an hour later.
Wrap every mutation in a transaction. Credits are a financial primitive wearing a costume. Any operation that reads a balance and then writes to it must be inside DB::transaction(), with a row lock on the account:
DB::transaction(function () use ($user, $amount) {
$account = CreditAccount::where('user_id', $user->id)->lockForUpdate()->firstOrFail();
$account->decrement('balance', $amount); // or the package's service call
});
Prefer the ledger, derive the balance. If the package ships a transaction/ledger table, treat it as the source of truth and the balance column as a cache. Reconciling SUM(amount) against balance is a cheap nightly check and catches nearly every bug in this class of code.
Idempotency for anything triggered by a job or webhook. Payment succeeded, order shipped, user signed up — each of these can fire twice. Store an idempotency key (order ID, event ID) with the credit transaction and reject duplicates at the unique-index level, not in PHP:
CreditTransaction::firstOrCreate(
['reference' => $order->id, 'type' => 'purchase'],
['user_id' => $user->id, 'amount' => $order->credit_amount]
);
Separate earning from spending. Two methods, two permission checks, two failure modes. Spending can fail (insufficient funds, expired credits, daily cap); earning almost never should. Conflating them into one adjustBalance($delta) produces code that is impossible to audit later.
Use domain events, not model observers, for side effects. "User crossed 1000 credits → unlock badge" belongs in a listener on a credit-changed event. Observers on the transaction model fire on reconciliation and admin adjustments too, which is usually wrong.
Admin adjustments deserve their own type. Give manual corrections a distinct type/actor field so support can answer "why did this balance change" without reading application logs. Every ledger row should carry type, actor_id (nullable for system), reference, and created_at.
Test with factories and SQLite in-memory. Credit logic is pure enough to unit test well. Assert the balance and the ledger row count — a test that only checks the balance will pass while silently double-writing.
0.1 + 0.2 problems in a points system produce support tickets that are genuinely hard to explain. If the package stores a decimal, cast to int at the boundary and keep it there.1.0.1 is a patch release with no published changelog detail — the release notes are only a compare link. Before bumping, run composer update tourze/credit-bundle --with-dependencies on a branch and read the diff, or at minimum git diff 1.0.0..1.0.1 in vendor. Patch versions on young packages sometimes carry schema or signature changes that the version number doesn't advertise.composer.json ("tourze/credit-bundle": "^1.0.1" rather than *) while the package is pre-2.0. You want a human in the loop for minor bumps.php artisan migrate will run them but you cannot edit them — any schema change must be a separate migration in your app.CHECK (balance >= 0) constraint or model guard, and a database row lock. Any one alone will eventually fail.php artisan schedule:run handling and verify it in staging — an unrun expiry job looks identical to correct behavior until the accounting is audited.php artisan config:clear before assuming your changes did nothing.CreditTransaction::where('user_id', $id)->orderBy('id')->get(['type','amount','created_at']) will show you the exact sequence, and the wrong row is usually visible within seconds.How can I help you explore Laravel packages today?