climactic/laravel-credits
Ledger-based credit system for Laravel: manage virtual currencies, reward points, and balances with deposits, withdrawals, transfers, transaction history, historical balance checks, and metadata support. Ideal for credit-based features in any app.
Installation:
composer require climactic/laravel-credits
php artisan vendor:publish --tag="credits-migrations"
php artisan migrate
Add Trait to Model:
use Climactic\Credits\Traits\HasCredits;
class User extends Model
{
use HasCredits;
}
First Transaction:
$user->creditAdd(100, 'Initial deposit');
creditAdd(), creditDeduct(), creditTransfer(), creditBalance()creditHistory()whereMetadata() scopesCreditsAdded, CreditsDeducted, CreditsTransferredImplement a subscription system where users earn credits for purchases and redeem them for premium features:
// User makes purchase
$user->creditAdd(50, 'Purchase #123', ['order_id' => 123, 'product' => 'Premium']);
// Check balance before redemption
if ($user->hasCredits(30)) {
$user->creditDeduct(30, 'Premium feature access');
}
// Add credits with metadata
$user->creditAdd(100, 'Referral bonus', ['referrer_id' => 5]);
// Deduct with validation
if ($user->hasCredits(20)) {
$user->creditDeduct(20, 'Service fee');
}
// Transfer between users
$sender->creditTransfer($recipient, 50, 'Gift');
// Paginated history with metadata
$history = $user->creditHistory()
->whereMetadata('source', 'purchase')
->paginate(10);
// Filter by date range
$recent = $user->creditHistory()
->whereBetween('created_at', [now()->subDays(7), now()])
->get();
// Bulk add credits to multiple users
User::where('role', 'premium')->each(function ($user) {
$user->creditAdd(100, 'Premium welcome bonus');
});
// Process refunds
$refunds = Order::where('status', 'refunded')->get();
foreach ($refunds as $order) {
$order->user->creditAdd($order->amount, 'Refund', ['order_id' => $order->id]);
}
// Track credit changes in logs
event(new CreditsAdded($user, $amount, $description));
// Send notifications
event(new CreditsDeducted($user, $amount, $description))
->then(function () use ($user) {
Notification::send($user, new CreditDeductedNotification());
});
// Return balance in API
return response()->json([
'balance' => $user->creditBalance(),
'transactions' => $user->creditHistory()->take(5)->get()
]);
// Process credit adjustments via Artisan
php artisan credits:adjust --user=1 --amount=50 --reason="Admin adjustment"
// Validate before deduction
$required = $subscription->requiredCredits();
if (!$user->hasCredits($required)) {
throw new \Exception("Insufficient credits for subscription");
}
// Calculate monthly credit trends
$monthly = $user->credits()
->selectRaw('MONTH(created_at) as month, SUM(amount) as total')
->groupBy('month')
->get();
// Extend for currency-aware credits
class User extends Model
{
use HasCredits;
public function creditAdd(float $amount, string $description, array $metadata = [], string $currency = 'USD')
{
$metadata['currency'] = $currency;
return $this->creditAdd($amount, $description, $metadata);
}
}
Concurrency Issues
creditAdd()/creditDeduct() in transactions or with explicit locking:
DB::transaction(function () use ($user) {
$user->creditDeduct(10);
});
Metadata Query Performance
Negative Balances
allow_negative_balance = false)Large Transaction Volumes
$users->chunk(100, function ($chunk) {
foreach ($chunk as $user) {
$user->creditAdd(50, 'Batch bonus');
}
});
Event Ordering
retryUntil for critical operations:
$user->creditDeduct(10)->retryUntil(function () {
return $user->fresh()->hasCredits(10);
});
Transaction Logs
DB::enableQueryLog();
$user->credits()->whereMetadata('source', 'purchase')->get();
dd(DB::getQueryLog());
Balance Mismatches
$manual = $user->credits()->sum('amount');
$running = $user->creditBalance();
Metadata Validation
MetadataValidator directly for custom validation:
use Climactic\Credits\Support\MetadataValidator;
$validator = new MetadataValidator();
$validator->validate('user.id', 123);
Table Name Overrides
'table_name' => 'custom_credits',
Negative Balance Handling
allow_negative_balance = true in config for overdraft scenariosEvent Customization
CreditsAdded::listen(function ($event) {
// Custom logic
});
Custom Transaction Types
// Add custom transaction logic
$user->credits()->create([
'amount' => 100,
'description' => 'Custom transaction',
'metadata' => ['type' => 'custom'],
'creditable_type' => User::class,
'creditable_id' => $user->id,
'running_balance' => $user->creditBalance() + 100
]);
Metadata Serialization
use Climactic\Credits\Support\MetadataSerializer;
class CustomSerializer extends MetadataSerializer
{
public function serialize($metadata)
{
// Custom serialization
}
}
// Register in service provider
$this->app->bind(MetadataSerializer::class, CustomSerializer::class);
Custom Query Scopes
// Add reusable scope
class User extends Model
{
public function scopeRecentCredits($query, $days = 7)
{
return $query->where('created_at', '>=', now()->subDays($days));
}
}
// Usage
$user->credits()->recentCredits()->get();
Audit Logging
CreditsAdded::listen(function ($event) {
AuditLog::create([
'user_id' => $event->user->id,
'action' => 'credits_added',
'details' => [
'amount' => $event->amount,
'description' => $event->description,
'metadata' => $event->metadata
]
]);
});
Indexing Strategy
CREATE INDEX idx_credits_user_type ON credits(creditable_type, creditable_id, created_at);
Batch Inserts
CreditBatchJob::dispatch($users, 100, 'Batch bonus');
Caching
$balance = Cache::remember("user_{$user->
How can I help you explore Laravel packages today?