climactic/laravel-credits
Ledger-based Laravel package for credit systems like virtual currency, reward points, and balances. Supports deposits/withdrawals, transfers, transaction history, metadata, and querying—ideal for building auditable, credit-based features in your app.
Installation:
composer require climactic/laravel-credits
php artisan vendor:publish --tag="credits-migrations"
php artisan migrate
Model Setup:
Add the HasCredits trait to your model (e.g., User):
use Climactic\Credits\Traits\HasCredits;
class User extends Model
{
use HasCredits;
}
First Transaction:
$user->creditAdd(100.00, 'Initial deposit');
creditAdd(), creditDeduct(), and creditBalance() methods.config/credits.php for settings like allow_negative_balance.credits table migration matches your needs.Subscription System:
// Add credits on subscription
$user->creditAdd(1000, 'Premium subscription activated', [
'subscription_id' => $subscription->id,
'plan' => 'premium',
]);
// Deduct credits on purchase
if ($user->hasCredits(50)) {
$user->creditDeduct(50, 'Product purchase', [
'product_id' => $product->id,
'order_id' => $order->id,
]);
}
Credit Management:
// Add credits with metadata
$user->creditAdd(50, 'Referral bonus', ['referrer_id' => $referrer->id]);
// Deduct credits with validation
if ($user->hasCredits(25)) {
$user->creditDeduct(25, 'Content download');
}
Transfers Between Users:
$sender->creditTransfer($recipient, 10, 'Tip', [
'source' => 'user_tip',
'transaction_id' => $tip->id,
]);
Audit Trails:
// Get recent transactions
$transactions = $user->creditHistory(10, 'desc');
// Filter by metadata
$purchases = $user->credits()
->whereMetadata('type', 'purchase')
->get();
Events: Listen to credits.added, credits.deducted, and credits.transferred for side effects (e.g., notifications, analytics).
event(new CreditAdded($user, 100, 'Deposit'));
Validation: Use hasCredits() before deductions to avoid negative balances:
if ($user->hasCredits($amount)) {
$user->creditDeduct($amount, 'Reason');
}
Batch Operations:
// Bulk add credits (e.g., for promotions)
User::where('role', 'premium')->each(function ($user) {
$user->creditAdd(50, 'Promotional bonus');
});
Historical Queries:
// Get balance at a specific time
$balance = $user->creditBalanceAt(new DateTime('2023-01-01'));
Custom Metadata Queries:
// Find all transactions with premium tags
$user->credits()
->whereMetadataContains('tags', 'premium')
->get();
Composite Filters:
$user->credits()
->whereMetadata('source', 'purchase')
->where('amount', '>', 50)
->orderBy('created_at', 'desc')
->get();
Performance Optimization:
creditHistoryWithMetadata() for complex filters:
$user->creditHistoryWithMetadata([
['key' => 'source', 'value' => 'subscription'],
['key' => 'amount', 'operator' => '>', 'value' => 100],
]);
Concurrency Issues:
DB::transaction(function () use ($user, $amount) {
$user->creditDeduct($amount, 'Safe transaction');
});
Metadata Query Performance:
credits table, slowing down applications with >10k transactions.Negative Balances:
allow_negative_balance is false (default), creditDeduct() throws an exception when insufficient credits exist.hasCredits() first or set allow_negative_balance to true in config.Metadata Key Validation:
->) throw InvalidArgumentException.$validKey = trim('source'); // Valid
$invalidKey = 'user->id'; // Invalid (throws exception)
Transaction History Order:
creditHistory() defaults to desc (newest first). Use 'asc' for chronological order:
$user->creditHistory(10, 'asc'); // Oldest first
Check Balance Mismatches:
SELECT * FROM credits WHERE creditable_id = ? AND creditable_type = ? ORDER BY created_at;
creditBalance() to spot discrepancies.Metadata Query Issues:
dd() to inspect metadata structure:
$transaction = $user->credits()->first();
dd($transaction->metadata); // Debug nested keys
Event Debugging:
AppServiceProvider@boot():
event(new CreditAdded($user, 100, 'Test'));
php artisan event:listen to check listeners.Custom Events: Extend the default events to include additional data:
class CustomCreditAdded implements ShouldBroadcast
{
public function __construct(
public User $user,
public float $amount,
public string $reason,
public array $metadata = []
) {}
}
Model Observers: Track credit changes in a separate table:
class CreditObserver
{
public function saved(HasCredits $model)
{
// Log to audit table
}
}
Query Scopes: Add reusable scopes to your model:
class User extends Model
{
public function scopeRecentCredits($query)
{
return $query->where('created_at', '>', now()->subDay());
}
}
Database Triggers: For complex logic, use database triggers (e.g., auto-logging to a secondary table).
Table Name:
Override the default credits table name in config/credits.php:
'table_name' => 'user_credits',
Negative Balances: Enable with caution:
'allow_negative_balance' => true,
Metadata Storage: The package stores metadata as JSON. For large datasets, consider:
Indexing Strategy:
WHERE clauses (e.g., source, order_id).[creditable_id, created_at, metadata->source]).Batch Inserts:
For bulk operations (e.g., promotions), use DB::transaction with batch inserts:
DB::transaction(function () {
$users->each(function ($user) {
$user->creditAdd(50, 'Bulk promotion');
});
});
Caching: Cache balances for read-heavy applications:
$balance
How can I help you explore Laravel packages today?