Laravel Credits

repository·main·Indexed 43 days ago

https://github.com/climactic/laravel-credits

A ledger-based package for managing credit-based systems such as virtual currencies and reward points within Laravel applications. It provides the HasCredits trait for models, supporting credit additions, deductions, transfers, and transaction history with metadata filtering and row-level locking for concurrency.

Tokens
5.8K
Snippets
15
Records
22
Agent score
56%

What's inside Laravel Credits

  1. Database recommendations for concurrency

    main

    Laravel Credits uses row-level locking (SELECT FOR UPDATE) to prevent race conditions during concurrent credit operations. To ensure data integrity in production, use a database engine that supports proper transaction isolation and row-level locking:

    • MySQL/MariaDB: Requires InnoDB engine.
    • PostgreSQL: Full support.
    • SQLite: Warning: Row-level locking is ignored; concurrent operations may produce incorrect results in high-concurrency scenarios.
  2. Install Laravel Credits

    main

    Install the package via Composer, publish the migrations, and run them to set up the necessary database schema. You can also optionally publish the configuration file.

    composer require climactic/laravel-credits
    
    php artisan vendor:publish --tag="credits-migrations"
    php artisan migrate
    
    # Optional: publish config
    php artisan vendor:publish --tag="credits-config"
  3. Optimize metadata query performance

    main

    Metadata queries perform full table scans by default. For large datasets (> 100k transactions), you should implement database-specific optimizations.

    MySQL/MariaDB: Virtual Columns

    Use virtual generated columns to extract JSON values into indexed columns.

    PostgreSQL: GIN Indexes

    Use a GIN (Generalized Inverted Index) on the metadata column to support all JSON operations efficiently.

    SQLite

    Metadata queries work but are slower on large datasets. Ensure the JSON1 extension is enabled (SQLite 3.38+).

    // Example: MySQL Virtual Column Migration
    return new class extends Migration
    {
        public function up()
        {
            $tableName = config('credits.table_name', 'credits');
    
            DB::statement("ALTER TABLE {$tableName} ADD COLUMN metadata_source VARCHAR(255) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.source'))) VIRTUAL");
            Schema::table($tableName, function (Blueprint $table) {
                $table->index('metadata_source');
            });
        }
    };
  4. Configure Laravel Credits

    main

    The package configuration allows you to control whether users can have negative balances and define the database table name used for credit transactions.

    return [
        // Allow negative balances
        'allow_negative_balance' => false,
    
        // Table name for credit transactions (change if you've updated the migration table name)
        'table_name' => 'credits',
    ];
  5. Understand the Credit model schema and relationships

    main

    The Credit model represents a single ledger entry. It is designed to be polymorphic, allowing it to be attached to various types of entities (users, accounts, etc.).

    Attributes

    • id: Unique identifier.
    • amount: The credit amount (cast to decimal:2).
    • description: A nullable string describing the transaction.
    • type: The type of credit transaction.
    • metadata: A JSON column for arbitrary data, cast to an array.
    • running_balance: The balance after this transaction (cast to decimal:2).
    • creditable_type: The polymorphic class name of the owner.
    • creditable_id: The ID of the polymorphic owner.

    Relationships

    • creditable(): A MorphTo relationship that returns the model instance the credit belongs to.
  6. Retrieve transaction history

    main

    Access a model's credit transactions using creditHistory() or creditHistoryWithMetadata().

    • creditHistory(int $limit = null, string $direction = 'desc'): Returns a collection of recent transactions. Use 'asc' or 'desc' for direction.
    • creditHistoryWithMetadata(array $filters, int $limit = null): Returns transactions filtered by specific metadata criteria.
    // Get last 10 transactions
    $history = $user->creditHistory();
    
    // Get last 20 transactions in ascending order
    $history = $user->creditHistory(20, 'asc');
  7. Use convenience methods for metadata filtering

    main

    For simpler workflows, use these convenience methods on the model:

    • creditsByMetadata(string $key, mixed $value, int $limit = null, string $order = 'desc', string $operator = '='): A shorthand for querying and limiting metadata-filtered transactions.
    • creditHistoryWithMetadata(array $filters, int $limit = null): Allows passing an array of filter definitions.

    Filter Array Syntax for creditHistoryWithMetadata:

    • Two-parameter style: ['key' => '...', 'value' => '...'] (uses equality).
    • Three-parameter style: ['key' => '...', 'operator' => '...', 'value' => '...'] (allows custom operators like > or <).
    • Null handling: To check for null, explicitly include 'value' => null in the array.
    // Get credits filtered by metadata (with limit and ordering)
    $purchases = $user->creditsByMetadata('source', 'purchase', limit: 20, order: 'desc');
    
    // With comparison operators
    $highValue = $user->creditsByMetadata('order_value', '>=', 100, limit: 10);
    
    // Multiple metadata filters
    $filtered = $user->creditHistoryWithMetadata([
        ['key' => 'source', 'value' => 'purchase'],
        ['key' => 'category', 'value' => 'electronics'],
        ['key' => 'tags', 'value' => 'premium', 'method' => 'contains'],
        ['key' => 'order_value', 'operator' => '>', 'value' => 50],
    ], limit: 25);
    
    // Query with null values
    $nullChecks = $user->creditHistoryWithMetadata([
        ['key' => 'refund_id', 'operator' => '=', 'value' => null],
        ['key' => 'status', 'value' => 'pending'],
    ], limit: 10);
  8. Available methods for credit management

    main

    The following methods are available on models using the Laravel Credits trait to manage balances and transactions:

    • creditAdd(float $amount, ?string $description = null, array $metadata = []): Add credits to the model.
    • creditDeduct(float $amount, ?string $description = null, array $metadata = []): Deduct credits from the model.
    • creditBalance(): Get the current balance.
    • creditTransfer(Model $recipient, float $amount, ?string $description = null, array $metadata = []): Transfer credits to another model.
    • creditHistory(int $limit = 10, string $order = 'desc'): Get transaction history.
    • hasCredits(float $amount): Check if model has enough credits.
    • creditBalanceAt(Carbon|DateTimeInterface|int $dateTime): Get balance at a specific time.
    • credits(): Eloquent relationship to credit transactions.
    • creditsByMetadata(string $key, $operator, $value = null, int $limit = 10, string $order = 'desc'): Get credits filtered by metadata key/value.
    • creditHistoryWithMetadata(array $filters, int $limit = 10, string $order = 'desc'): Get credits filtered by multiple metadata.
  9. Manage credits and balances

    main

    The HasCredits trait provides methods to manipulate a model's credit balance and check availability.

    • creditAdd(float $amount, string $description, array $metadata = []): Adds credits to the balance.
    • creditDeduct(float $amount, string $description): Deducts credits from the balance.
    • creditBalance(): Returns the current credit balance.
    • hasCredits(float $amount): Returns true if the model has enough credits for the specified amount.
    • creditBalanceAt(DateTimeInterface $date): Returns the balance as it was on a specific date.
    // Add credits
    $user->creditAdd(100.00, 'Subscription Activated');
    
    // Deduct credits
    $user->creditDeduct(50.00, 'Purchase Made');
    
    // Get current balance
    $balance = $user->creditBalance();
    
    // Check if user has enough credits
    if ($user->hasCredits(30.00)) {
        // Proceed with transaction
    }
    
    // Historical balance
    $date = new DateTime('2023-01-01');
    $balanceAsOf = $user->creditBalanceAt($date);
  10. Query scopes for the credits relationship

    main

    You can use these scopes when querying the credits() Eloquent relationship to filter transactions based on their metadata:

    • whereMetadata(string $key, $operator, $value = null): Filter by metadata key/value.
    • whereMetadataContains(string $key, $value): Filter where metadata array contains value.
    • whereMetadataHas(string $key): Filter where metadata key exists.
    • whereMetadataNull(string $key): Filter where metadata key is null/doesn't exist.
    • whereMetadataLength(string $key, $operator, $value): Filter by metadata array length.
    // Example usage on the relationship
    $user->credits()->whereMetadata('source', '=', 'api')->get();