Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Laravel Credits
https://github.com/climactic/laravel-credits
Admin
A ledger-based Laravel package for managing credit-based systems including virtual currencies,
...
Tokens:
12,495
Snippets:
97
Trust Score:
8.2
Update:
13 hours ago
Context
Skills
Chat
Benchmark
96.5
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Laravel Credits Laravel Credits is a ledger-based Laravel package for managing credit-based systems in your application. It provides a complete solution for virtual currencies, reward points, subscription credits, or any credit-based feature requiring accurate balance tracking and transaction history. The package uses a running balance approach with row-level locking to ensure data integrity during concurrent operations. The core functionality centers around the `HasCredits` trait, which can be added to any Eloquent model to enable credit operations. All transactions are stored as ledger entries with support for metadata, descriptions, and historical balance lookups. Events are dispatched after each credit operation, enabling integrations with notifications, analytics, and other application components. ## Installation Install the package via Composer and publish the migrations to set up the credits table in your database. ```bash # Install the package composer require climactic/laravel-credits # Publish and run migrations php artisan vendor:publish --tag="credits-migrations" php artisan migrate # Optionally publish the config file php artisan vendor:publish --tag="credits-config" ``` ## Configuration The package supports configurable options for negative balances and custom table names. ```php // config/credits.php return [ // When false, attempting to deduct more credits than available throws InsufficientCreditsException 'allow_negative_balance' => false, // The database table name for credit transactions 'table_name' => 'credits', ]; ``` ## Setting Up Your Model Add the `HasCredits` trait to any Eloquent model that should handle credits, enabling all credit management methods on that model. ```php <?php namespace App\Models; use Climactic\Credits\Traits\HasCredits; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use HasCredits; // Your existing model code... } ``` ## creditAdd() Adds credits to a model's balance with an optional description and metadata. The method creates a new credit transaction record, updates the running balance, and dispatches a `CreditsAdded` event. Uses database transactions with row-level locking to prevent race conditions. ```php // Basic credit addition $user->creditAdd(100.00, 'Welcome bonus'); // Add credits with metadata for tracking $credit = $user->creditAdd(50.00, 'Purchase reward', [ 'order_id' => 12345, 'product' => 'Premium Subscription', 'source' => 'referral', 'tags' => ['bonus', 'promotion'] ]); // Access the returned Credit model echo $credit->id; // Transaction ID echo $credit->amount; // 50.00 echo $credit->running_balance; // New balance after addition echo $credit->type; // 'credit' echo $credit->metadata; // ['order_id' => 12345, ...] ``` ## creditDeduct() Deducts credits from a model's balance. Throws `InsufficientCreditsException` if the balance would go negative (unless `allow_negative_balance` is enabled). Creates a debit transaction record and dispatches a `CreditsDeducted` event. ```php use Climactic\Credits\Exceptions\InsufficientCreditsException; // Basic deduction $user->creditDeduct(25.00, 'Feature unlock'); // Deduction with metadata $credit = $user->creditDeduct(10.00, 'API call charges', [ 'api_endpoint' => '/v1/generate', 'tokens_used' => 1500, 'request_id' => 'req_abc123' ]); // Handle insufficient credits try { $user->creditDeduct(1000.00, 'Large purchase'); } catch (InsufficientCreditsException $e) { // $e->getMessage() returns: "Insufficient credits. Requested: 1000, Available: 115" return response()->json([ 'error' => 'Not enough credits', 'message' => $e->getMessage() ], 402); } ``` ## creditBalance() Retrieves the current credit balance for the model by reading the most recent running balance from the transaction history. Returns 0.0 if no transactions exist. ```php // Get current balance $balance = $user->creditBalance(); echo "Your balance: {$balance} credits"; // Use in conditional logic if ($user->creditBalance() >= 100) { // Enable premium features } // Display formatted balance $formatted = number_format($user->creditBalance(), 2); echo "Balance: \${$formatted}"; ``` ## hasCredits() Checks if the model has at least the specified amount of credits available. Returns a boolean, useful for pre-validating transactions before attempting them. ```php // Check before making a purchase $itemCost = 75.00; if ($user->hasCredits($itemCost)) { $user->creditDeduct($itemCost, 'Item purchase'); return response()->json(['success' => true]); } return response()->json(['error' => 'Insufficient credits'], 402); // Use in authorization logic public function purchaseItem(Request $request, Item $item) { if (!$request->user()->hasCredits($item->price)) { abort(403, 'Not enough credits to purchase this item'); } // Process purchase... } ``` ## creditTransfer() Transfers credits from one model to another atomically. Uses deterministic locking to prevent deadlocks when concurrent transfers occur in opposite directions. Returns an array with both balances after the transfer and dispatches a `CreditsTransferred` event. ```php // Basic transfer between users $sender = User::find(1); $recipient = User::find(2); $result = $sender->creditTransfer($recipient, 50.00, 'Payment for service'); // $result contains: // [ // 'sender_balance' => 150.00, // Sender's new balance // 'recipient_balance' => 250.00 // Recipient's new balance // ] // Transfer with metadata $result = $sender->creditTransfer( $recipient, 100.00, 'Freelance project payment', [ 'project_id' => 'proj_456', 'invoice_number' => 'INV-2024-001', 'service' => 'web_development' ] ); // Handle transfer errors try { $sender->creditTransfer($recipient, 10000.00, 'Large transfer'); } catch (InsufficientCreditsException $e) { Log::warning('Transfer failed', [ 'sender_id' => $sender->id, 'recipient_id' => $recipient->id, 'amount' => 10000.00, 'error' => $e->getMessage() ]); } ``` ## creditHistory() Retrieves the transaction history for a model. Supports limiting the number of records and ordering by ascending or descending date. Results are deterministically ordered by `created_at` and `id`. ```php // Get last 10 transactions (default) $history = $user->creditHistory(); // Get last 50 transactions in descending order $history = $user->creditHistory(50, 'desc'); // Get oldest 20 transactions first $history = $user->creditHistory(20, 'asc'); // Display transaction history foreach ($user->creditHistory(25) as $transaction) { $type = $transaction->type === 'credit' ? '+' : '-'; $date = $transaction->created_at->format('Y-m-d H:i'); echo "{$date}: {$type}{$transaction->amount} - {$transaction->description}\n"; echo " Balance after: {$transaction->running_balance}\n"; if ($transaction->metadata) { echo " Metadata: " . json_encode($transaction->metadata) . "\n"; } } ``` ## creditBalanceAt() Retrieves the historical balance at a specific point in time. Accepts a DateTimeInterface, Carbon instance, or Unix timestamp (seconds or milliseconds). Useful for generating statements or auditing past balances. ```php use Carbon\Carbon; // Get balance at a specific date $date = new DateTime('2024-01-01'); $balanceJan1 = $user->creditBalanceAt($date); // Using Carbon $lastMonth = Carbon::now()->subMonth(); $balanceLastMonth = $user->creditBalanceAt($lastMonth); // Using Unix timestamp (seconds) $timestamp = strtotime('2024-06-15'); $balance = $user->creditBalanceAt($timestamp); // Using Unix timestamp (milliseconds - auto-detected) $milliseconds = 1718409600000; $balance = $user->creditBalanceAt($milliseconds); // Generate a monthly statement $startOfMonth = Carbon::now()->startOfMonth(); $endOfMonth = Carbon::now()->endOfMonth(); $openingBalance = $user->creditBalanceAt($startOfMonth); $closingBalance = $user->creditBalanceAt($endOfMonth); echo "Opening Balance: {$openingBalance}\n"; echo "Closing Balance: {$closingBalance}\n"; echo "Net Change: " . ($closingBalance - $openingBalance) . "\n"; ``` ## credits() Relationship Access the underlying Eloquent relationship for advanced queries. Returns a MorphMany relationship to Credit records, allowing you to chain any Eloquent query methods. ```php // Get all credits as a collection $allCredits = $user->credits()->get(); // Count total transactions $totalTransactions = $user->credits()->count(); // Get only credit (additions) or debit (deductions) transactions $additions = $user->credits()->where('type', 'credit')->get(); $deductions = $user->credits()->where('type', 'debit')->get(); // Sum total credits added $totalAdded = $user->credits()->where('type', 'credit')->sum('amount'); $totalDeducted = $user->credits()->where('type', 'debit')->sum('amount'); // Get transactions from this month $thisMonth = $user->credits() ->whereMonth('created_at', now()->month) ->whereYear('created_at', now()->year) ->get(); // Paginate transaction history $paginatedHistory = $user->credits() ->orderBy('created_at', 'desc') ->paginate(15); ``` ## whereMetadata() Scope Filter credit transactions by metadata key/value pairs. Supports dot notation for nested keys and comparison operators. Keys are validated to prevent SQL injection. ```php // Simple equality query $purchases = $user->credits() ->whereMetadata('source', 'purchase') ->get(); // Using comparison operators $highValue = $user->credits() ->whereMetadata('order_value', '>', 100) ->get(); // Query nested metadata using dot notation $specificUser = $user->credits() ->whereMetadata('user.id', 123) ->get(); // Multiple conditions $filtered = $user->credits() ->whereMetadata('source', 'api') ->whereMetadata('tokens_used', '>=', 1000) ->where('amount', '>', 5) ->get(); // Combine with other query methods $recentApiCharges = $user->credits() ->whereMetadata('source', 'api') ->where('type', 'debit') ->where('created_at', '>=', now()->subDays(7)) ->orderBy('created_at', 'desc') ->get(); ``` ## whereMetadataContains() Scope Filter transactions where a metadata array contains a specific value. Useful for querying tags, categories, or any array-based metadata fields. ```php // Find transactions tagged as 'premium' $premiumTransactions = $user->credits() ->whereMetadataContains('tags', 'premium') ->get(); // Find transactions with multiple required tags $featured = $user->credits() ->whereMetadataContains('tags', 'premium') ->whereMetadataContains('tags', 'featured') ->get(); // Combine with other filters $recentPremium = $user->credits() ->whereMetadataContains('tags', 'premium') ->where('created_at', '>=', now()->subMonth()) ->where('type', 'credit') ->get(); ``` ## whereMetadataHas() and whereMetadataNull() Scopes Check for the existence or absence of metadata keys. `whereMetadataHas()` finds records where the key exists and is not null. `whereMetadataNull()` finds records where the key is null or doesn't exist. ```php // Find transactions that have an order_id $withOrders = $user->credits() ->whereMetadataHas('order_id') ->get(); // Find transactions without refund information $noRefunds = $user->credits() ->whereMetadataNull('refund_id') ->get(); // Find pending transactions (no completion timestamp) $pending = $user->credits() ->whereMetadataHas('initiated_at') ->whereMetadataNull('completed_at') ->get(); // Combine existence checks $manualTransactions = $user->credits() ->whereMetadataNull('api_source') ->whereMetadataNull('automation_id') ->get(); ``` ## whereMetadataLength() Scope Filter transactions by the length of array metadata fields. Useful for finding transactions with multiple tags or items. ```php // Find transactions with more than one tag $multiTagged = $user->credits() ->whereMetadataLength('tags', '>', 1) ->get(); // Find transactions with exactly 3 items $threeItems = $user->credits() ->whereMetadataLength('items', 3) ->get(); // Combine with other filters $complexTransactions = $user->credits() ->whereMetadataLength('tags', '>=', 2) ->whereMetadata('source', 'bulk_purchase') ->where('amount', '>', 100) ->get(); ``` ## creditsByMetadata() Convenience method to get credit history filtered by a single metadata key/value pair with limit and ordering support. ```php // Get last 10 purchase transactions $purchases = $user->creditsByMetadata('source', 'purchase'); // Get last 20 transactions with specific source, newest first $apiCharges = $user->creditsByMetadata('source', 'api', limit: 20, order: 'desc'); // Using comparison operators $highValue = $user->creditsByMetadata('order_value', '>=', 100, limit: 50); // Get oldest matching transactions $oldestReferrals = $user->creditsByMetadata('source', 'referral', limit: 10, order: 'asc'); ``` ## creditHistoryWithMetadata() Advanced filtering method that accepts multiple metadata conditions with different query methods. Supports equality, contains, has, null, and length filters. ```php // Multiple filter conditions $filtered = $user->creditHistoryWithMetadata([ ['key' => 'source', 'value' => 'purchase'], ['key' => 'category', 'value' => 'electronics'], ['key' => 'order_value', 'operator' => '>', 'value' => 50], ], limit: 25); // Mix different query methods $complex = $user->creditHistoryWithMetadata([ ['key' => 'source', 'value' => 'api'], // whereMetadata ['key' => 'tags', 'value' => 'premium', 'method' => 'contains'], // whereMetadataContains ['key' => 'order_id', 'method' => 'has'], // whereMetadataHas ['key' => 'refund_id', 'method' => 'null'], // whereMetadataNull ['key' => 'items', 'operator' => '>', 'value' => 2, 'method' => 'length'], // whereMetadataLength ], limit: 100); // Query with null values explicitly $pendingRefunds = $user->creditHistoryWithMetadata([ ['key' => 'refund_status', 'operator' => '=', 'value' => null], ['key' => 'refund_requested', 'method' => 'has'], ], limit: 50); ``` ## orWhereMetadata() Scopes OR condition versions of all metadata query scopes for building complex queries with alternative conditions. ```php // Find transactions from either source $apiOrManual = $user->credits() ->whereMetadata('source', 'api') ->orWhereMetadata('source', 'manual') ->get(); // Find transactions with either tag $premiumOrFeatured = $user->credits() ->whereMetadataContains('tags', 'premium') ->orWhereMetadataContains('tags', 'featured') ->get(); // Complex OR conditions $results = $user->credits() ->where(function ($query) { $query->whereMetadata('source', 'purchase') ->orWhereMetadata('source', 'subscription'); }) ->where('amount', '>', 10) ->get(); ``` ## Event Listeners The package dispatches events after each credit operation. All events implement `ShouldDispatchAfterCommit` to ensure they only fire after the database transaction commits successfully. ```php // app/Listeners/SendCreditNotification.php <?php namespace App\Listeners; use Climactic\Credits\Events\CreditsAdded; use Climactic\Credits\Events\CreditsDeducted; use Climactic\Credits\Events\CreditsTransferred; class SendCreditNotification { public function handleCreditsAdded(CreditsAdded $event): void { // Access event properties $user = $event->creditable; $amount = $event->amount; $newBalance = $event->newBalance; $transactionId = $event->transactionId; $description = $event->description; $metadata = $event->metadata; // Send notification $user->notify(new CreditsReceivedNotification($amount, $newBalance)); // Log the transaction Log::info('Credits added', [ 'user_id' => $user->id, 'transaction_id' => $transactionId, 'amount' => $amount, 'new_balance' => $newBalance, 'metadata' => $metadata ]); } public function handleCreditsDeducted(CreditsDeducted $event): void { if ($event->newBalance < 100) { $event->creditable->notify(new LowBalanceWarning($event->newBalance)); } } public function handleCreditsTransferred(CreditsTransferred $event): void { // Notify both parties $event->sender->notify(new CreditsSentNotification( $event->amount, $event->recipient, $event->senderNewBalance )); $event->recipient->notify(new CreditsReceivedNotification( $event->amount, $event->sender, $event->recipientNewBalance )); } } // Register in EventServiceProvider protected $listen = [ CreditsAdded::class => [SendCreditNotification::class . '@handleCreditsAdded'], CreditsDeducted::class => [SendCreditNotification::class . '@handleCreditsDeducted'], CreditsTransferred::class => [SendCreditNotification::class . '@handleCreditsTransferred'], ]; ``` ## Database Performance Optimization For high-volume applications, add database indexes on frequently queried metadata keys to improve query performance. ```php // MySQL/MariaDB: Create virtual columns with indexes <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; return new class extends Migration { public function up(): void { $tableName = config('credits.table_name', 'credits'); // Add virtual column for frequently queried 'source' key DB::statement(" ALTER TABLE {$tableName} ADD COLUMN metadata_source VARCHAR(255) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.source'))) VIRTUAL "); // Add virtual column for 'order_id' key DB::statement(" ALTER TABLE {$tableName} ADD COLUMN metadata_order_id BIGINT GENERATED ALWAYS AS (JSON_EXTRACT(metadata, '$.order_id')) VIRTUAL "); // Create indexes on virtual columns Schema::table($tableName, function (Blueprint $table) { $table->index('metadata_source'); $table->index('metadata_order_id'); // Composite index for common query patterns $table->index(['creditable_id', 'creditable_type', 'metadata_source']); }); } }; // PostgreSQL: Use GIN indexes on JSONB return new class extends Migration { public function up(): void { $tableName = config('credits.table_name', 'credits'); // GIN index for all JSON operations DB::statement(" CREATE INDEX {$tableName}_metadata_gin_idx ON {$tableName} USING GIN (metadata) "); // Path-specific indexes for common queries DB::statement(" CREATE INDEX {$tableName}_metadata_source_idx ON {$tableName} ((metadata->>'source')) "); } }; ``` ## Summary Laravel Credits provides a complete solution for implementing credit-based systems in Laravel applications. The primary use cases include virtual currency systems for SaaS platforms, reward points for e-commerce, API usage tracking and billing, subscription credit allocation, and peer-to-peer payment features. The ledger-based architecture ensures accurate balance tracking with full audit trails, while the event system enables seamless integration with notifications, analytics, and external services. Integration follows standard Laravel patterns - add the `HasCredits` trait to any model, and all credit operations become available immediately. The package handles concurrency through database transactions and row-level locking, supports flexible metadata for contextual transaction data, and provides powerful query scopes for filtering transaction history. For production deployments, the documentation includes database-specific optimization guides to ensure performance at scale with MySQL/MariaDB virtual columns or PostgreSQL GIN indexes.