climactic/laravel-credits
Ledger-based credits for Laravel: manage virtual currencies, reward points, and other credit systems with deposits, withdrawals, transfers, transaction history, historical balances, and metadata-powered querying.
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 = User::first();
$user->creditAdd(100.00, 'Initial deposit');
creditAdd(), creditDeduct(), and creditBalance().creditHistory() for transaction logs.whereMetadata() for filtering transactions.Subscription System:
// Add credits on subscription
$user->creditAdd(1000, 'Annual subscription', ['plan' => 'premium']);
// Deduct credits for purchases
if ($user->hasCredits(50)) {
$user->creditDeduct(50, 'Product purchase', ['product_id' => 123]);
}
// Check balance
$balance = $user->creditBalance();
Credit Management:
// Add credits with metadata
$user->creditAdd(50, 'Referral bonus', ['referrer_id' => 42]);
// Deduct credits with validation
if ($user->hasCredits(25)) {
$user->creditDeduct(25, 'Service fee');
}
Transfers:
// Transfer between users
$user1->creditTransfer($user2, 30, 'Collaboration payment');
Historical Queries:
// Get last 5 transactions
$user->creditHistory(5);
// Filter by metadata
$user->credits()
->whereMetadata('source', 'purchase')
->get();
Events: Listen to CreditsAdded, CreditsDeducted, or CreditsTransferred for real-time actions.
event(new CreditsAdded($user, $amount, $description));
Middleware: Validate credits before critical actions:
public function handle(Request $request, Closure $next) {
if (!$request->user()->hasCredits(10)) {
abort(403, 'Insufficient credits');
}
return $next($request);
}
Commands: Automate credit adjustments (e.g., nightly interest):
$users = User::all();
foreach ($users as $user) {
$user->creditAdd($user->creditBalance() * 0.01, 'Nightly interest');
}
Batch Processing:
$users = User::where('tier', 'premium')->get();
foreach ($users as $user) {
$user->creditAdd(100, 'Premium tier bonus');
}
Custom Metadata Validation: Extend the trait to validate metadata before transactions:
protected function validateMetadata(array $metadata): void {
if (empty($metadata['order_id'])) {
throw new \InvalidArgumentException('Order ID required');
}
}
Concurrency Issues:
DB::transaction(function () use ($user) {
$user->creditDeduct(50, 'Purchase');
});
Metadata Query Performance:
credits table.Negative Balances:
allow_negative_balance = false).Metadata Key Format:
user->id) throw exceptions.user.id) and validate keys early.Transaction Logs:
$user->credits()->latest()->take(10)->get();
Use dd() to inspect metadata structure.
Balance Mismatches:
SELECT SUM(amount) FROM credits WHERE creditable_id = $userId;
Event Debugging: Listen to events in Tinker:
event(new CreditsAdded($user, 100, 'Test'));
Custom Events: Extend the package’s events for your use case:
class CustomCreditEvent extends CreditsAdded {
public function __construct($creditable, $amount, $description, $customData) {
parent::__construct($creditable, $amount, $description);
$this->customData = $customData;
}
}
Query Scopes:
Add custom scopes to the HasCredits trait:
public function scopeRecentTransactions($query, $days = 7) {
return $query->where('created_at', '>=', now()->subDays($days));
}
Metadata Serialization:
Override serializeMetadata() to customize how metadata is stored:
protected function serializeMetadata(array $metadata): string {
return json_encode($metadata, JSON_UNESCAPED_SLASHES);
}
Table Name:
Change config('credits.table_name') if you renamed the migration table.
Decimal Precision:
Ensure your database column for amount uses DECIMAL(19, 4) for precision.
Time Zones:
Historical balances (creditBalanceAt()) respect the model’s usesTimezone setting.
Indexing Strategy: Prioritize indexes for:
creditable_id, creditable_type (foreign keys).source, order_id).Batch Inserts:
Use DB::transaction() for bulk operations to avoid lock contention:
DB::transaction(function () {
foreach ($users as $user) {
$user->creditAdd(10, 'Batch bonus');
}
});
How can I help you explore Laravel packages today?