### Install TokenLedger Gem and Migrations Source: https://context7.com/wuliwong/token_ledger/llms.txt Instructions for adding the TokenLedger gem to a Rails application, installing dependencies, and generating/running database migrations. Supports custom owner models. ```bash # Add to Gemfile gem "token_ledger" # Install dependencies and generate migrations bundle install rails generate token_ledger:install rails db:migrate # For custom owner model (default is User) rails generate token_ledger:install --owner-model=Team ``` -------------------------------- ### Install Token Ledger and Generate Migrations Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Commands to install the gem, generate Token Ledger's database migrations, and run the migrations. It also details the two default migrations created. ```bash bundle install rails generate token_ledger:install rails db:migrate ``` -------------------------------- ### Generate Token Ledger Install with Custom Owner Model Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Example of how to generate the Token Ledger installation with a custom owner model, such as 'Team', instead of the default 'User'. ```bash rails generate token_ledger:install --owner-model=Team ``` -------------------------------- ### Install Token Ledger Gem Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Instructions for adding the Token Ledger gem to your project's Gemfile, including options for the latest unreleased code from GitHub. ```ruby gem "token_ledger" ``` ```ruby gem "token_ledger", git: "https://github.com/wuliwong/token_ledger", branch: "main" ``` -------------------------------- ### Performing a Token Deposit in Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Shows how to add tokens to a user's account using the `TokenLedger::Manager.deposit` method. It includes examples for simple deposits and deposits with external tracking to ensure idempotency, preventing duplicate processing of the same transaction. ```ruby ```ruby # Simple deposit TokenLedger::Manager.deposit( owner: user, amount: 100, description: "Token purchase" ) # Deposit with external tracking (for idempotency) TokenLedger::Manager.deposit( owner: user, amount: 100, description: "Subscription renewal", external_source: "stripe", external_id: "inv_123456", # Prevents duplicate processing metadata: { plan: "pro", period: "monthly" } ) # Will raise DuplicateTransactionError if called again with same external_source + external_id ``` ``` -------------------------------- ### Database Index Optimization - SQL Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Provides SQL examples for adding database indexes to optimize common query patterns within the Token Ledger system. These indexes are crucial for improving the performance of transaction history lookups, filtering by transaction type, and efficiently retrieving account balances. ```sql # For transaction history queries add_index :ledger_transactions, [:owner_type, :owner_id, :created_at] # For transaction type filtering add_index :ledger_transactions, [:transaction_type, :created_at] # For account balance lookups add_index :ledger_accounts, :current_balance ``` -------------------------------- ### Query User Transaction History Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Provides examples for retrieving a user's transaction history, including fetching recent transactions, filtering by transaction type (deposit or spend), and finding a specific transaction by its external source and ID. It also shows how to access the ledger entries associated with a transaction. ```ruby # Get user's transaction history user.ledger_transactions.order(created_at: :desc).limit(20) # Filter by type user.ledger_transactions.where(transaction_type: "deposit") user.ledger_transactions.where(transaction_type: "spend") # Find specific transaction txn = TokenLedger::LedgerTransaction.find_by( external_source: "stripe", external_id: "inv_123" ) # Get entries for a transaction txn.ledger_entries.each do |entry| puts "#{entry.account.name}: #{entry.entry_type} #{entry.amount}" end ``` -------------------------------- ### Define Account Codes Convention in Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Illustrates the hierarchical account code convention used for organizing tokens within the ledger. It shows examples for user-specific wallets, system-wide token sources, and token sinks, emphasizing their role in tracking token flow rather than traditional accounting. ```ruby # Wallets (user-specific) "wallet:#{user.id}" # Main balance "wallet:#{user.id}:reserved" # Reserved tokens # Token Sources (system-wide - where tokens enter) "source:stripe" # Purchased via Stripe "source:paypal" # Purchased via PayPal "source:promo" # Promotional grants "source:referral" # Referral bonuses "source:admin" # Manual admin credits # Token Sinks (system-wide - where tokens leave) "sink:consumed" # Tokens consumed for service delivery "sink:refunded" # Refunded to customer "sink:expired" # Tokens expired ``` -------------------------------- ### Integrate with Stripe using Pay Gem Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Details the steps to integrate TokenLedger with Stripe using the Pay Gem, including installing the gem, configuring the User model, setting up a webhook handler for Stripe events (like invoice payments and checkout completions), and defining Stripe Products with metadata for token amounts. ```ruby # Gemfile gem 'pay' bundle install rails pay:install rails db:migrate ``` ```ruby class User < ApplicationRecord pay_customer has_many :ledger_transactions, as: :owner, class_name: "TokenLedger::LedgerTransaction" end ``` ```ruby # config/routes.rb post "/webhooks/stripe", to: "webhooks/stripe#create" # app/controllers/webhooks/stripe_controller.rb class Webhooks::StripeController < ApplicationController skip_before_action :verify_authenticity_token def create event = Stripe::Webhook.construct_event( request.body.read, request.env['HTTP_STRIPE_SIGNATURE'], ENV['STRIPE_WEBHOOK_SECRET'] ) case event.type when 'invoice.payment_succeeded' handle_subscription_payment(event.data.object) when 'checkout.session.completed' handle_onetime_purchase(event.data.object) end head :ok rescue Stripe::SignatureVerificationError head :bad_request end private def handle_subscription_payment(invoice) user = User.find_by(pay_customer_id: invoice.customer) return unless user credits = invoice.lines.data.first.price.metadata['monthly_credits'].to_i TokenLedger::Manager.deposit( owner: user, amount: credits, description: "Subscription: #{invoice.lines.data.first.price.nickname}", external_source: "stripe", external_id: invoice.id, # Prevents duplicate credits metadata: { invoice_id: invoice.id, subscription_id: invoice.subscription, plan: invoice.lines.data.first.price.nickname } ) end def handle_onetime_purchase(session) user = User.find_by(pay_customer_id: session.customer) return unless user credits = session.metadata['token_amount'].to_i TokenLedger::Manager.deposit( owner: user, amount: credits, description: "Token purchase", external_source: "stripe", external_id: session.id, metadata: { session_id: session.id, amount_paid: session.amount_total / 100.0 } ) end end ``` ```text # In Stripe Dashboard or via API, add metadata to Price objects: # metadata: { monthly_credits: "1000" } # metadata: { monthly_credits: "3500" } # metadata: { monthly_credits: "12500" } ``` -------------------------------- ### Deposit Tokens with TokenLedger Manager Source: https://context7.com/wuliwong/token_ledger/llms.txt Demonstrates how to deposit tokens into a user's wallet using TokenLedger::Manager.deposit. Supports simple deposits and deposits with external source tracking for idempotency. ```ruby # Simple deposit transaction_id = TokenLedger::Manager.deposit( owner: user, amount: 100, description: "Token purchase" ) # Deposit with external tracking (prevents duplicate processing) transaction_id = TokenLedger::Manager.deposit( owner: user, amount: 500, description: "Subscription renewal", external_source: "stripe", external_id: "inv_abc123", metadata: { plan: "pro", period: "monthly" } ) # => Creates entries: # Debit wallet:123 500 (user balance +500) # Credit source:stripe 500 (liability +500) # Calling again with same external_source + external_id raises error begin TokenLedger::Manager.deposit( owner: user, amount: 500, external_source: "stripe", external_id: "inv_abc123", description: "Duplicate attempt" ) rescue TokenLedger::DuplicateTransactionError => e puts "Already processed: #{e.message}" end ``` -------------------------------- ### Configure Owner Model with Ledger Transactions Association Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Example of how to add the `has_many :ledger_transactions` association to your owner model (e.g., `User`) to link it with Token Ledger transactions. Includes an optional helper method for balance. ```ruby class User < ApplicationRecord has_many :ledger_transactions, as: :owner, class_name: "TokenLedger::LedgerTransaction" # Optional: Add helper method for balance def balance cached_balance end end ``` -------------------------------- ### Reserve, Capture, and Release Tokens with Token Ledger (Ruby) Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates the manual reserve, capture, and release pattern for managing tokens with external API calls. It shows how to reserve tokens, perform an external operation, and then either capture the tokens on success or release them on failure. Includes error handling and idempotency considerations using external_source and external_id. ```ruby reservation_id = TokenLedger::Manager.reserve( owner: user, amount: 50, description: "Reserve for API call", metadata: { job_id: "job_123" } ) try # Step 2: Call external API (this can't be rolled back) result = ExternalAPI.expensive_operation(job_id: "job_123") # Step 3: Capture the reserved tokens (mark as consumed) # For idempotency with external job systems, use external_source/external_id TokenLedger::Manager.capture( reservation_id: reservation_id, description: "API call completed", external_source: "job_runner", external_id: "job_123:capture" # Prevents duplicate capture on retry ) rescue => e # Step 3b: Release reserved tokens back to wallet on failure TokenLedger::Manager.release( reservation_id: reservation_id, description: "API call failed - refund", external_source: "job_runner", external_id: "job_123:release", metadata: { error: e.message } ) raise e end ``` -------------------------------- ### Run Token Ledger Tests Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Provides the command to execute the comprehensive test suite for the Token Ledger gem. This ensures all functionalities, including thread safety and concurrency, are working as expected. ```bash cd gems/token_ledger bundle exec rake test ``` -------------------------------- ### Handle TokenLedger Errors in Ruby Source: https://context7.com/wuliwong/token_ledger/llms.txt Demonstrates how to rescue various TokenLedger-specific exceptions like InsufficientFundsError, DuplicateTransactionError, ImbalancedTransactionError, and AccountNotFoundError. This ensures robust error handling for token-related operations. ```ruby begin TokenLedger::Manager.spend(owner: user, amount: 100, description: "Service") rescue TokenLedger::InsufficientFundsError => e # User doesn't have enough tokens flash[:error] = "Not enough tokens. Current balance: #{user.cached_balance}" redirect_to purchase_tokens_path rescue TokenLedger::DuplicateTransactionError => e # Already processed (idempotency protection) Rails.logger.warn "Duplicate transaction attempt: #{e.message}" # Safe to ignore - transaction already completed rescue TokenLedger::ImbalancedTransactionError => e # Internal error - should never happen in normal operation Rails.logger.error "CRITICAL: Ledger imbalance: #{e.message}" Bugsnag.notify(e) raise e rescue TokenLedger::AccountNotFoundError => e # Account doesn't exist (usually during reconciliation) Rails.logger.error "Account not found: #{e.message}" end ``` -------------------------------- ### Reserve Tokens with TokenLedger Manager Source: https://context7.com/wuliwong/token_ledger/llms.txt Illustrates reserving tokens using TokenLedger::Manager.reserve. This moves tokens from the available balance to a reserved account, preparing for external operations. ```ruby # Reserve tokens before calling external API reservation_id = TokenLedger::Manager.reserve( owner: user, amount: 50, description: "Reserve for OpenAI API call", metadata: { job_id: "job_789", model: "gpt-4" } ) # => Creates entries: # Credit wallet:123 50 (available balance -50) # Debit wallet:123:reserved 50 (reserved balance +50) # User's available balance decreased, but total tokens still held puts user.reload.cached_balance # Available balance (excludes reserved) # Check reserved account balance reserved_account = TokenLedger::LedgerAccount.find_by(code: "wallet:#{user.id}:reserved") puts reserved_account.current_balance # => 50 ``` -------------------------------- ### Seed System Accounts for TokenLedger Source: https://context7.com/wuliwong/token_ledger/llms.txt Creates essential system accounts required for token management, such as token sources (e.g., 'stripe', 'paypal') and token sinks (e.g., 'consumed', 'refunded'). These accounts are typically set up during the application's seeding process. ```ruby # db/seeds.rb or db/seeds/token_ledger.rb # Token Sources (where tokens enter the system) %w[stripe paypal promo referral admin].each do |source| TokenLedger::LedgerAccount.find_or_create_by!(code: "source:#{source}") do |account| account.name = "#{source.capitalize} Token Source" account.current_balance = 0 account.metadata = {} end end # Token Sinks (where tokens leave the system) %w[consumed refunded expired].each do |sink| TokenLedger::LedgerAccount.find_or_create_by!(code: "sink:#{sink}") do |account| account.name = "Tokens #{sink.capitalize}" account.current_balance = 0 account.metadata = {} end end ``` -------------------------------- ### Calculate and Reconcile Token Balances Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates how to calculate a user's token balance directly from ledger entries for accuracy and how to reconcile a cached balance with the calculated balance. This ensures data consistency between different storage mechanisms. ```ruby actual_balance = TokenLedger::Balance.calculate("wallet:#{user.id}") TokenLedger::Balance.reconcile_user!(user) user.reload user.cached_balance ``` -------------------------------- ### Manual Token Crediting Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Highlights the payment-processor-agnostic nature of TokenLedger, allowing users to credit tokens manually or through other payment processors. This demonstrates the flexibility of the system for custom integration scenarios. ```ruby # TokenLedger is completely payment-processor agnostic. You can credit tokens from any source: ``` -------------------------------- ### Deposit Tokens with TokenLedger::Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates various scenarios for depositing tokens into a user's wallet using the TokenLedger::Manager.deposit method. Supports admin credits, webhook integrations (e.g., PayPal), promotional bonuses, and referral rewards, with optional metadata for additional context. ```ruby TokenLedger::Manager.deposit( owner: user, amount: 500, description: "Admin credit - customer support", external_source: "admin", external_id: "admin_#{current_admin.id}_#{Time.now.to_i}", metadata: { admin_id: current_admin.id, reason: "Apology for service issue" } ) TokenLedger::Manager.deposit( owner: user, amount: 1000, description: "PayPal purchase", external_source: "paypal", external_id: paypal_transaction_id ) TokenLedger::Manager.deposit( owner: user, amount: 100, description: "Welcome bonus", external_source: "promo", external_id: "signup_bonus_#{user.id}" ) TokenLedger::Manager.deposit( owner: referrer, amount: 50, description: "Referral bonus", external_source: "referral", external_id: "referral_#{referred_user.id}", metadata: { referred_user_id: referred_user.id } ) ``` -------------------------------- ### Find or Create Account in Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Finds an existing account by its code and name, or creates a new one if it doesn't exist. This method is thread-safe to prevent race conditions. It requires unique account code and name parameters. ```ruby TokenLedger::Account.find_or_create(code: "wallet:#{user.id}", name: "User Wallet") ``` -------------------------------- ### Balance Caching Comparison - Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates the difference between reading a cached balance and calculating the balance directly from ledger entries. The cached balance is faster for reads, while calculating is slower but provides the accurate, up-to-date balance. This is crucial for performance optimization in high-traffic applications. ```ruby # Fast (uses cached value) if user.cached_balance >= cost # proceed end # Slow (calculates from all entries) if TokenLedger::Balance.calculate("wallet:#{user.id}") >= cost # proceed end ``` -------------------------------- ### Spend Tokens with TokenLedger Manager Source: https://context7.com/wuliwong/token_ledger/llms.txt Shows how to deduct tokens from a user's wallet using TokenLedger::Manager.spend. This method is for immediate deductions and raises InsufficientFundsError if the balance would be negative. ```ruby # Simple spend transaction_id = TokenLedger::Manager.spend( owner: user, amount: 10, description: "Image generation" ) # => Creates entries: # Credit wallet:123 10 (user balance -10) # Debit sink:consumed 10 (consumed +10) # Spend with metadata tracking transaction_id = TokenLedger::Manager.spend( owner: user, amount: 25, description: "Video processing", metadata: { resolution: "1080p", duration_seconds: 30, job_id: "job_456" } ) # Handle insufficient funds begin TokenLedger::Manager.spend( owner: user, amount: 10000, description: "Large operation" ) rescue TokenLedger::InsufficientFundsError => e puts "Not enough tokens: #{e.message}" # => "Account wallet:123 would go negative (balance: 465, delta: -10000)" end ``` -------------------------------- ### Configure Owner Model for TokenLedger Source: https://context7.com/wuliwong/token_ledger/llms.txt Integrates your application's owner model (e.g., User, Team) with TokenLedger. This involves setting up a `has_many` association for `ledger_transactions` and optionally defining helper methods for balance access or broadcasting balance updates via ActionCable. ```ruby # app/models/user.rb class User < ApplicationRecord has_many :ledger_transactions, as: :owner, class_name: "TokenLedger::LedgerTransaction" # Helper method for balance access def balance cached_balance end # Optional: broadcast balance updates via ActionCable def broadcast_balance broadcast_replace_to( "user_#{id}_balance", target: "balance_display", partial: "shared/balance", locals: { balance: cached_balance } ) end end # Usage user = User.find(1) puts user.balance # Fast cached read puts user.ledger_transactions.count # Transaction history ``` -------------------------------- ### Capture Reserved Tokens with TokenLedger Manager Source: https://context7.com/wuliwong/token_ledger/llms.txt Explains how to capture previously reserved tokens using TokenLedger::Manager.capture after a successful external operation. Consumes tokens from the reserved account. ```ruby # After successful API call, capture the reserved tokens transaction_id = TokenLedger::Manager.capture( reservation_id: reservation_id, description: "OpenAI API call completed successfully", metadata: { tokens_used: 1250, response_id: "resp_xyz" } ) # => Creates entries: ``` -------------------------------- ### Custom Adjustments with TokenLedger::Manager.adjust Source: https://context7.com/wuliwong/token_ledger/llms.txt Allows for manual creation of adjustment transactions, including corrections, reversals, and manual credits/debits. Entries must be balanced (total debits must equal total credits). Supports external_source, external_id, and metadata for tracking. ```ruby # Reverse a previous transaction original = TokenLedger::LedgerTransaction.find(transaction_id) TokenLedger::Manager.adjust( owner: original.owner, description: "Reversal of transaction ##{original.id} - customer complaint", external_source: "admin", external_id: "reversal_#{original.id}", metadata: { reason: "Service not delivered", admin_id: current_admin.id }, entries: original.ledger_entries.map { |entry| { account_code: entry.account.code, account_name: entry.account.name, type: entry.entry_type == "debit" ? :credit : :debit, # Swap type amount: entry.amount } } ) ``` ```ruby # Manual admin credit TokenLedger::Manager.adjust( owner: user, description: "Goodwill credit for service issue", external_source: "admin", external_id: "credit_#{Time.current.to_i}_#{current_admin.id}", entries: [ { account_code: "wallet:#{user.id}", account_name: "User Wallet", type: :debit, amount: 100 }, { account_code: "source:admin", account_name: "Admin Credits", type: :credit, amount: 100 } ] ) ``` -------------------------------- ### Integrate Stripe Webhooks with TokenLedger in Ruby Source: https://context7.com/wuliwong/token_ledger/llms.txt Shows how to process Stripe payment success events, find the corresponding user, and credit tokens using TokenLedger. It includes idempotency protection using external_id and handles potential signature verification errors. ```ruby # app/controllers/webhooks/stripe_controller.rb class Webhooks::StripeController < ApplicationController skip_before_action :verify_authenticity_token def create event = Stripe::Webhook.construct_event( request.body.read, request.env["HTTP_STRIPE_SIGNATURE"], ENV["STRIPE_WEBHOOK_SECRET"] ) case event.type when "invoice.payment_succeeded" invoice = event.data.object user = User.find_by(stripe_customer_id: invoice.customer) return head :ok unless user credits = invoice.lines.data.first.price.metadata["monthly_credits"].to_i TokenLedger::Manager.deposit( owner: user, amount: credits, description: "Subscription: #{invoice.lines.data.first.price.nickname}", external_source: "stripe", external_id: invoice.id, # Idempotency key metadata: { invoice_id: invoice.id, subscription_id: invoice.subscription, amount_paid_cents: invoice.amount_paid } ) end head :ok rescue Stripe::SignatureVerificationError head :bad_request rescue TokenLedger::DuplicateTransactionError head :ok # Already processed, safe to acknowledge end end ``` -------------------------------- ### Find or Create Ledger Account with TokenLedger Source: https://context7.com/wuliwong/token_ledger/llms.txt A thread-safe method to find an existing account or create a new one. This is primarily used internally by Manager methods but can also be utilized for custom account creation. It's particularly useful for setting up system accounts like sources and sinks, or pre-creating user wallet accounts. ```ruby # Find or create system accounts (run in seeds) source_account = TokenLedger::Account.find_or_create( code: "source:stripe", name: "Stripe Token Purchases" ) sink_account = TokenLedger::Account.find_or_create( code: "sink:consumed", name: "Tokens Consumed" ) # User wallet accounts are created automatically by deposit/reserve # But can be pre-created if needed wallet = TokenLedger::Account.find_or_create( code: "wallet:#{user.id}", name: "User #{user.id} Wallet" ) puts "Account: #{wallet.code}, Balance: #{wallet.current_balance}" ``` -------------------------------- ### Querying Token Ledger Transactions and Relationships (Ruby) Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Shows how to query the Token Ledger for specific transactions, such as finding a reservation and its associated capture or release transactions. It also demonstrates how to find the parent transaction of a capture or release operation. ```ruby # Find a reservation and its child transactions reservation = TokenLedger::LedgerTransaction.find(reservation_id) captures = TokenLedger::LedgerTransaction.where( parent_transaction_id: reservation_id, transaction_type: "capture" ) releases = TokenLedger::LedgerTransaction.where( parent_transaction_id: reservation_id, transaction_type: "release" ) # Find the parent of a capture capture_txn = TokenLedger::LedgerTransaction.find_by(transaction_type: "capture") parent = TokenLedger::LedgerTransaction.find(capture_txn.parent_transaction_id) if capture_txn.parent_transaction_id ``` -------------------------------- ### Reversing a Transaction in Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates how to reverse a previous token transaction by creating a new transaction that effectively cancels out the original. This is the recommended approach for correcting errors, as direct updates or deletions are prevented by immutability constraints. ```ruby ```ruby # Wrong: Don't do this transaction.destroy # Will fail due to FK constraint # Right: Create a reversing transaction by swapping debit/credit on same accounts original_transaction = TokenLedger::LedgerTransaction.find(transaction_id) TokenLedger::Manager.adjust( owner: original_transaction.owner, description: "Reversal of transaction ##{original_transaction.id}", entries: original_transaction.ledger_entries.map { |entry| { account_code: entry.account.code, account_name: entry.account.name, type: entry.entry_type == 'debit' ? :credit : :debit, # Swap entry type amount: entry.amount } } ) ``` ``` -------------------------------- ### Query Transactions and Entries with TokenLedger Source: https://context7.com/wuliwong/token_ledger/llms.txt Allows querying transaction history and auditing ledger entries for reporting and debugging purposes. You can retrieve a user's transaction history, filter transactions by type, find transactions by external reference, and view the detailed ledger entries associated with a transaction. ```ruby # Get user's transaction history user.ledger_transactions.order(created_at: :desc).limit(20).each do |txn| puts "#{txn.created_at.strftime('%Y-%m-%d %H:%M')} | #{txn.transaction_type.ljust(10)} | #{txn.description}" end # Filter by transaction type deposits = user.ledger_transactions.where(transaction_type: "deposit") spends = user.ledger_transactions.where(transaction_type: "spend") # Find by external reference txn = TokenLedger::LedgerTransaction.find_by( external_source: "stripe", external_id: "inv_abc123" ) # View entries for a transaction txn.ledger_entries.each do |entry| sign = entry.entry_type == "debit" ? "+" : "-" puts " #{sign}#{entry.amount} #{entry.account.name} (#{entry.account.code})" end # Find reservation and its child transactions reservation = TokenLedger::LedgerTransaction.find(reservation_id) captures = reservation.child_transactions.where(transaction_type: "capture") releases = reservation.child_transactions.where(transaction_type: "release") ``` -------------------------------- ### Create Token Ledger Seed Accounts Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Ruby code for `db/seeds.rb` to create essential Token Ledger accounts, including token sources (e.g., Stripe, PayPal) and token sinks (e.g., consumed, refunded). ```ruby # db/seeds.rb or db/seeds/token_ledger.rb # TOKEN SOURCES (where tokens enter the system) TokenLedger::LedgerAccount.find_or_create_by!(code: "source:stripe") do |account| account.name = "Tokens Purchased via Stripe" end TokenLedger::LedgerAccount.find_or_create_by!(code: "source:paypal") do |account| account.name = "Tokens Purchased via PayPal" end TokenLedger::LedgerAccount.find_or_create_by!(code: "source:promo") do |account| account.name = "Promotional Token Grants" end TokenLedger::LedgerAccount.find_or_create_by!(code: "source:referral") do |account| account.name = "Referral Bonuses" end TokenLedger::LedgerAccount.find_or_create_by!(code: "source:admin") do |account| account.name = "Admin Manual Credits" end # TOKEN SINKS (where tokens leave the system) TokenLedger::LedgerAccount.find_or_create_by!(code: "sink:consumed") do |account| account.name = "Tokens Consumed (Service Delivered)" end TokenLedger::LedgerAccount.find_or_create_by!(code: "sink:refunded") do |account| account.name = "Tokens Refunded" end TokenLedger::LedgerAccount.find_or_create_by!(code: "sink:expired") do |account| account.name = "Tokens Expired" end ``` -------------------------------- ### Reserve Tokens with TokenLedger::Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Shows how to reserve tokens using TokenLedger::Manager.reserve. This moves tokens from the user's wallet to a reserved account, preventing immediate spending. It requires owner, amount, and description, with optional metadata. ```ruby TokenLedger::Manager.reserve(owner: user, amount: 20, description: "Pending order") ``` -------------------------------- ### Migrate Existing Integer Columns to Token Ledger Source: https://github.com/wuliwong/token_ledger/blob/main/README.md A Ruby migration script to move balances from simple integer columns (e.g., 'credits') to the Token Ledger system. It includes steps for depositing existing balances and optionally removing the old column. ```ruby # db/migrate/XXXXXX_migrate_to_token_ledger.rb class MigrateToTokenLedger < ActiveRecord::Migration[7.0] def up # Ensure TokenLedger tables exist # (Run `rails generate token_ledger:install` first) # Migrate existing balances User.find_each do |user| next if user.credits.zero? # Skip users with no balance TokenLedger::Manager.deposit( owner: user, amount: user.credits, description: "Balance migration from legacy credits column", external_source: "migration", external_id: "user_#{user.id}_migration", metadata: { legacy_credits: user.credits, migrated_at: Time.current.iso8601 } ) end # Optional: Remove old column after verifying migration # remove_column :users, :credits end def down # Restore credits from ledger if needed User.find_each do |user| wallet = TokenLedger::LedgerAccount.find_by(code: "wallet:#{user.id}") user.update_column(:credits, wallet&.current_balance || 0) if wallet end end end ``` -------------------------------- ### Deposit Tokens Using TokenLedger Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates how to deposit tokens for a given owner using the TokenLedger::Manager. This operation creates two balanced entries: a debit to the owner's wallet and a credit to the source (e.g., Stripe). It ensures that the total debits equal the total credits for an atomic transaction. ```ruby TokenLedger::Manager.deposit(owner: user, amount: 100, description: "Token purchase") ``` -------------------------------- ### Calculate Balance in Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Calculates the actual balance for a given account or account code by summing ledger entries (debits - credits). It accepts either a LedgerAccount object or an account code string. ```ruby TokenLedger::Balance.calculate(account_or_code) ``` -------------------------------- ### Reserve Tokens Using TokenLedger Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Shows how to reserve tokens using TokenLedger::Manager. This operation moves tokens from the main wallet to a reserved state, creating a credit to the wallet and a debit to a reserved account. This is useful for holding tokens before final confirmation of an action. ```ruby TokenLedger::Manager.reserve(owner: user, amount: 30, description: "Hold for API call") ``` -------------------------------- ### Automatic Reserve/Capture/Release with spend_with_api Source: https://context7.com/wuliwong/token_ledger/llms.txt A convenience method that automates the reserve, capture, and release process. It reserves tokens, executes a given block of code, captures tokens on success, or releases them on failure. This simplifies common workflows involving external API calls. ```ruby # Automatic handling of external API calls result = TokenLedger::Manager.spend_with_api( owner: user, amount: 50, description: "AI image generation", metadata: { prompt: "sunset over mountains" } ) do # This block runs OUTSIDE the database transaction # If it fails, tokens are automatically released ImageGenerationAPI.generate(prompt: "sunset over mountains") end ``` ```ruby # Handle failures gracefully begin TokenLedger::Manager.spend_with_api( owner: user, amount: 100, description: "Video rendering" ) do VideoAPI.render(params) # Raises error end rescue VideoAPI::RenderError => e # Tokens automatically released, user can retry puts "Rendering failed, tokens refunded: #{e.message}" end ``` -------------------------------- ### Handle Token Ledger Errors in Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates how to handle common exceptions raised by the Token Ledger gem, such as InsufficientFundsError, DuplicateTransactionError, and ImbalancedTransactionError. This allows for graceful error management in your application. ```ruby begin TokenLedger::Manager.spend(owner: user, amount: 100, description: "Image generation") rescue TokenLedger::InsufficientFundsError => e # Handle insufficient balance flash[:error] = "Not enough tokens. Please purchase more." rescue TokenLedger::DuplicateTransactionError => e # Already processed this transaction Rails.logger.warn "Duplicate transaction: #{e.message}" rescue TokenLedger::ImbalancedTransactionError => e # Internal error - debits don't equal credits Rails.logger.error "Ledger imbalance: #{e.message}" Bugsnag.notify(e) end ``` -------------------------------- ### Batch User Deposits with Transaction - Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Illustrates how to perform batch operations, specifically crediting multiple users with tokens, within a single database transaction. This ensures atomicity, meaning either all deposits succeed or none do, preventing partial updates and maintaining data integrity. It uses ActiveRecord::Base.transaction for this purpose. ```ruby ActiveRecord::Base.transaction do users.each do |user| TokenLedger::Manager.deposit( owner: user, amount: 50, description: "Promotional credit" ) end end ``` -------------------------------- ### Using Association Methods for Token Ledger Transactions (Ruby) Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Demonstrates the usage of the added `child_transactions` and `parent_transaction` association methods on `LedgerTransaction` objects. This allows for more intuitive navigation and querying of the transaction hierarchy. ```ruby reservation.child_transactions.where(transaction_type: "capture") capture_txn.parent_transaction ``` -------------------------------- ### Reserve Tokens with TokenLedger::Manager Source: https://context7.com/wuliwong/token_ledger/llms.txt Reserves a specified amount of tokens for a given owner. This is the first step in a multi-step token transaction. It requires owner, amount, and description. The returned reservation_id is used for subsequent capture or release operations. ```ruby reservation_id = TokenLedger::Manager.reserve( owner: user, amount: 100, description: "Reserve for batch processing" ) ``` -------------------------------- ### Capture Reserved Tokens with TokenLedger::Manager Source: https://context7.com/wuliwong/token_ledger/llms.txt Captures a portion or all of the reserved tokens. This is used when the actual usage of tokens differs from the initially reserved amount. It requires the reservation_id, the amount to capture, and a description. Optional parameters include external_source and external_id for idempotency. ```ruby TokenLedger::Manager.capture( reservation_id: reservation_id, amount: 75, # Only capture 75 of the 100 reserved description: "Batch processing completed", external_source: "job_runner", external_id: "batch_001:capture" ) ``` -------------------------------- ### Integrate with Stripe Directly (No Pay Gem) Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Explains how to integrate TokenLedger with Stripe without using the Pay Gem. This involves adding the Stripe gem, adding a `stripe_customer_id` to the User model, and setting up a webhook handler to process Stripe payment events and deposit tokens accordingly. ```ruby # Gemfile gem 'stripe' ``` ```bash rails generate migration AddStripeCustomerIdToUsers stripe_customer_id:string rails db:migrate ``` ```ruby class Webhooks::StripeController < ApplicationController skip_before_action :verify_authenticity_token def create event = Stripe::Webhook.construct_event( request.body.read, request.env['HTTP_STRIPE_SIGNATURE'], ENV['STRIPE_WEBHOOK_SECRET'] ) case event.type when 'invoice.payment_succeeded' handle_payment(event.data.object) end head :ok end private def handle_payment(invoice) user = User.find_by(stripe_customer_id: invoice.customer) return unless user credits = invoice.lines.data.first.price.metadata['monthly_credits'].to_i TokenLedger::Manager.deposit( owner: user, amount: credits, description: "Payment received", external_source: "stripe", external_id: invoice.id, metadata: { invoice_id: invoice.id } ) end end ``` -------------------------------- ### Verify Token Ledger Migration Accuracy Source: https://github.com/wuliwong/token_ledger/blob/main/README.md A Ruby script to verify that the migration from legacy integer columns to Token Ledger has been accurate by comparing balances. ```ruby # Verify migration accuracy User.find_each do |user| legacy = user.credits ledger = TokenLedger::LedgerAccount.find_by(code: "wallet:#{user.id}")&.current_balance || 0 if legacy != ledger puts "MISMATCH: User #{user.id} - Legacy: #{legacy}, Ledger: #{ledger}" end end ``` -------------------------------- ### Adding Association Methods to LedgerTransaction Model (Ruby) Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Illustrates how to extend the `LedgerTransaction` model to include convenient association methods like `child_transactions` and `parent_transaction`. This simplifies querying the relationships between different ledger transactions. ```ruby # Add to gems/token_ledger/app/models/token_ledger/ledger_transaction.rb class TokenLedger::LedgerTransaction < ApplicationRecord belongs_to :parent_transaction, class_name: "TokenLedger::LedgerTransaction", optional: true has_many :child_transactions, class_name: "TokenLedger::LedgerTransaction", foreign_key: :parent_transaction_id end ``` -------------------------------- ### Reconcile Balances with TokenLedger Source: https://context7.com/wuliwong/token_ledger/llms.txt Updates cached balances to match calculated values from ledger entries. This method is useful for fixing balance drift or during periodic maintenance. It can reconcile a specific account, an entire user (updating both account and user cached_balance), or be used for batch reconciliation of all users. ```ruby # Reconcile a specific account TokenLedger::Balance.reconcile!("wallet:#{user.id}") # Reconcile user (updates both account and user cached_balance) TokenLedger::Balance.reconcile_user!(user) user.reload puts "Reconciled balance: #{user.cached_balance}" # Batch reconciliation for all users User.find_each do |u| begin TokenLedger::Balance.reconcile_user!(u) rescue TokenLedger::AccountNotFoundError # User has no wallet yet, skip next end end ``` -------------------------------- ### Calculate True Balance with TokenLedger::Balance.calculate Source: https://context7.com/wuliwong/token_ledger/llms.txt Calculates the actual balance of an account by summing all its ledger entries. This method is accurate but can be slow, making it suitable for reconciliation or verification rather than frequent balance checks. Prefer cached_balance for regular reads. ```ruby # Calculate balance from ledger entries (slow but accurate) actual_balance = TokenLedger::Balance.calculate("wallet:#{user.id}") puts "Calculated balance: #{actual_balance}" ``` ```ruby # Compare with cached balance cached_balance = user.cached_balance account_cached = TokenLedger::LedgerAccount.find_by(code: "wallet:#{user.id}")&.current_balance puts "User cached_balance: #{cached_balance}" puts "Account current_balance: #{account_cached}" puts "Calculated from entries: #{actual_balance}" # Check if drift has occurred if actual_balance != cached_balance puts "WARNING: Balance drift detected!" end ``` -------------------------------- ### Accessing User Balance with Token Ledger (Ruby) Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Shows how to retrieve a user's current balance using the denormalized `cached_balance` attribute provided by the Token Ledger. This method is optimized for fast reads as it avoids database JOINs. ```ruby # Get current balance (from cache - fast) user.cached_balance # or user.balance if you added the helper method ``` -------------------------------- ### Spend Tokens Using TokenLedger Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Illustrates spending tokens with the TokenLedger::Manager. This action results in a credit to the owner's wallet and a debit to a consumption sink (e.g., 'consumed'). This maintains the double-entry principle where every transaction is balanced. ```ruby TokenLedger::Manager.spend(owner: user, amount: 50, description: "Service consumed") ``` -------------------------------- ### Spend Tokens with TokenLedger::Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Illustrates how to deduct tokens from a user's wallet using TokenLedger::Manager.spend. This method is designed for immediate deduction and is safe by design. It requires owner, amount, and description, with optional metadata. ```ruby TokenLedger::Manager.spend(owner: user, amount: 10, description: "Image generation") ``` -------------------------------- ### Release Reserved Tokens with TokenLedger::Manager Source: https://github.com/wuliwong/token_ledger/blob/main/README.md Illustrates releasing reserved tokens back to the user's wallet using TokenLedger::Manager.release. This is useful for cancellations or failed transactions. It requires the reservation_id and a description, with optional amount, external_source, external_id, and metadata. ```ruby TokenLedger::Manager.release(reservation_id: reservation.id, description: "Order cancelled", external_source: "order_system", external_id: "order_123_cancel") ``` -------------------------------- ### Troubleshoot Balance Drift - Ruby Source: https://github.com/wuliwong/token_ledger/blob/main/README.md A Ruby code snippet to diagnose and fix balance discrepancies between cached balances and actual balances calculated from ledger entries. It first checks for drift, prints a message if detected, and then calls `TokenLedger::Balance.reconcile_user!` to correct the cached balance. ```ruby # Check actual balance from entries actual = TokenLedger::Balance.calculate("wallet:#{user.id}") cached = user.cached_balance if actual != cached puts "Balance drift detected: actual=#{actual}, cached=#{cached}" # Fix it TokenLedger::Balance.reconcile_user!(user) end ```