usage_credits

repository·main·Indexed 41 days ago

https://github.com/rameerez/usage_credits

A Rails gem providing a production-grade, double-entry ledger system for managing user credits, tokens, or money-like balances. Designed for SaaS, AI apps, and marketplaces, it supports usage-based pricing, subscription fulfillment, and detailed audit trails. It integrates with the pay gem for Stripe/PayPal payments and requires an ActiveJob backend for recurring credit refills.

Tokens
14.2K
Snippets
49
Records
63
Agent score
42%

What's inside usage_credits

  1. How credit fulfillment cycles work

    main

    Credit fulfillment is decoupled from billing periods. While pay handles the user's subscription payments (billing cycles), usage_credits handles how credits are distributed (fulfillment cycles).

    Requirements:

    • You must have an ActiveJob backend (e.g., Sidekiq, solid_queue) configured.
    • You must schedule the fulfillment job to run periodically to trigger credit refills.

    You can match fulfillment to billing (e.g., monthly) or use custom intervals like :day, :quarter, or specific durations like 15.days.

    subscription_plan :pro do
      gives 10_000.credits.every(15.days)
    end
  2. Understand row-level locking and concurrency

    main

    To prevent double-spending and race conditions, the gem acquires a row-level lock on the wallet during credit-spending operations.

    Important: The row remains locked for the duration of the credit-spending block. While most operations are short, avoid performing long-running, CPU-bound tasks (like heavy AI generation) inside the spending block, as this will block all other concurrent wallet operations for that specific user.

  3. How the double-entry ledger architecture works

    main

    The gem is built on a production-grade double-entry ledger system rather than a simple balance attribute. Key components include:

    • Wallet: The root object for all credit operations (add/deduct).
    • Transaction: The immutable, append-only log of all operations.
    • Fulfillment: Orchestrates credit-giving actions (recurring or one-time).
    • Allocation: Enables a FIFO (First-In-First-Out) bucket-based spending system, which is essential for handling expiring credits.
    • Operation: Represents a credit-spending action.

    This architecture ensures auditability, prevents double-spending via row-level locking, and handles complex scenarios like expiring balances and decoupled billing cycles.

  4. Configure and sell credit packs

    main

    usage_credits integrates with the pay gem to handle credit pack sales.

    Prerequisite: You must have the pay gem installed and configured with a payment processor (Stripe, PayPal, etc.).

    1. Define Packs: Define your packs in config/initializers/usage_credits.rb using the credit_pack DSL. 2. Purchase Packs: Use the provided helpers to create checkout sessions for your users.

    Automatic Handling:

    • Fulfillment after successful payment.
    • Proportional credit removal on refunds.
    • Prevention of double-processing.
    • Support for multiple currencies.
    # 1. Define in config/initializers/usage_credits.rb
    credit_pack :starter do
      gives 1000.credits
      costs 49.dollars
    end
    
    # 2. Use in your controller to initiate purchase
    credit_pack = UsageCredits.find_credit_pack(:starter)
    session = credit_pack.create_checkout_session(current_user)
    redirect_to session.url
  5. Install usage_credits in a Rails app

    main

    To add usage-based credits to your Rails application, add the gem to your Gemfile and run the installation commands. You must also have an ActiveJob backend (like Sidekiq or solid_queue) configured for subscription credit fulfillment, and it is recommended to use the pay gem for Stripe/PayPal integrations.

    # Gemfile
    gem 'usage_credits'
    bundle install
    rails generate usage_credits:install
    rails db:migrate
  6. Configure recurring credit fulfillment

    main

    The gem relies on UsageCredits::FulfillmentJob to refill credits for subscriptions. You must schedule this job to run periodically using your ActiveJob backend's recurring task feature.

    Example configuration for solid_queue in config/recurring.yml:

    production:
      refill_credits:
        class: UsageCredits::FulfillmentJob
        queue: default
        schedule: every 5 minutes
    # config/recurring.yml
    
    production:
      refill_credits:
        class: UsageCredits::FulfillmentJob
        queue: default
        schedule: every 5 minutes
  7. Configure lifecycle callbacks for credit events

    main

    You can hook into various credit-related events using callbacks in your usage_credits.rb initializer. These callbacks are useful for analytics, notifications, or triggering business logic like upselling.

    Important: Callbacks are executed synchronously during the credit operation. To prevent slowing down your application, avoid heavy operations inside callbacks. Instead, use them to enqueue background jobs (e.g., using deliver_later or perform_later).

    Available Callbacks:

    • on_credits_added: After credits are added to a wallet
    • on_credits_deducted: After credits are deducted from a wallet
    • on_low_balance_reached: When balance drops below threshold (fires once per crossing)
    • on_balance_depleted: When balance reaches exactly zero
    • on_insufficient_credits: When an operation fails due to insufficient credits
    • on_credit_pack_purchased: After a credit pack purchase is fulfilled
    • on_subscription_credits_awarded: After subscription credits are awarded
    UsageCredits.configure do |config|
      # Example: Notify user when balance is low
      config.on_low_balance_reached do |ctx|
        LowCreditsMailer.buy_more(ctx.owner, remaining: ctx.new_balance).deliver_later
      end
    
      # Example: Log failed operations
      config.on_insufficient_credits do |ctx|
        Rails.logger.info "[Credits] User #{ctx.owner.id} needs #{ctx.amount}, has #{ctx.metadata[:available]}"
      end
    end
  8. Extend transaction categories for custom business logic

    main

    By default, the gem includes categories like signup_bonus, operation_charge, and subscription_credits. You can extend these with your own categories (e.g., for payments, payouts, or refunds) using the additional_categories configuration option. Custom categories are validated, tracked in transaction history, and available for filtering.

    # config/initializers/usage_credits.rb
    UsageCredits.configure do |config|
      # Add custom categories for your business logic
      config.additional_categories = %
        payment_received
        payment_sent
        payout_requested
        platform_fee
        refund
        tip
        cashback
      ]
    end
  9. Manage user credits with the Wallet model

    main

    The UsageCredits::Wallet model is the central authority for managing a user's credit balance, transaction history, and credit operations. It supports polymorphic ownership (e.g., belongs_to :owner, polymorphic: true), allowing you to attach a wallet to any model in your Rails application.

    Key responsibilities include:

    • Tracking real-time credit balances.
    • Performing atomic credit operations (adding/deducting).
    • Managing credit expiration (FIFO-based allocation).
    • Handling low balance alerts via callbacks.
  10. How credit fulfillment works

    main

    The UsageCredits::FulfillmentService manages the lifecycle of awarding credits to a user's wallet. The process follows these steps:

    1. Validation: Checks if the fulfillment has a valid type (subscription, credit_pack, or manual), an associated wallet, and the required metadata.
    2. Locking: Uses an ActiveRecord transaction and a row-level lock (@fulfillment.lock!) to prevent double-awarding credits if multiple processes attempt to fulfill the same record.
    3. Calculation:
      • subscription: Looks up the plan via UsageCredits.find_subscription_plan_by_processor_id and uses credits_per_period.
      • credit_pack: Looks up the pack via UsageCredits.find_credit_pack and uses total_credits.
      • manual: Uses the credits value provided in the metadata.
    4. Awarding: Calls wallet.add_credits with specific categories (subscription_credits, credit_pack_purchase, or credit_added) and calculates expiration if applicable.
    5. Updating: Updates the fulfillment record with last_fulfilled_at, the amount of credits given, and calculates the next_fulfillment_at timestamp.
  11. How subscription credit fulfillment works

    main

    The integration uses a combination of subscription lifecycle events and a Fulfillment object to manage recurring credits.

    1. Triggering: The extension uses after_commit hooks on the subscription model to react to status and plan changes.
    2. The Fulfillment Object: A UsageCredits::Fulfillment record acts as the source of truth for the credit schedule. It tracks next_fulfillment_at (when the next batch of credits should be awarded) and stops_at (when the recurring awarding should cease).
    3. Immediate vs. Scheduled:
      • Immediate: Credits for the current period (or trial/signup bonuses) are awarded immediately upon subscription activation or upgrade.
      • Scheduled: The UsageCredits::FulfillmentJob (not shown in this file, but referenced) uses the Fulfillment record to perform the actual periodic awarding in the future.
    4. Reactivation: If a user moves from a non-credit plan back to a credit plan, the extension can identify and reactivate a previously stopped Fulfillment record.
  12. Define recurring subscription plans with CreditSubscriptionPlan

    main

    Use the CreditSubscriptionPlan DSL to define subscription plans that automatically grant credits to users on a recurring basis. The actual credit fulfillment is handled by the PaySubscriptionExtension, which monitors subscription events (creation, renewal, etc.) and adds credits to the user's wallet.

    Key configuration options include:

    • Base credits: Set how many credits are given each period using gives(amount).
    • Signup bonus: Set one-time credits given upon signup using signup_bonus(amount).
    • Trial credits: Set credits provided during a trial period using trial_includes(amount).
    • Rollover: Control if unused credits carry over to the next period using unused_credits(:rollover).
    • Expiration on cancellation: Control if credits expire when a subscription is cancelled using expire_after(duration) (where duration is an ActiveSupport::Duration).
    • Metadata: Attach custom data using meta(hash).
    UsageCredits::CreditSubscriptionPlan.new("Pro Plan").tap do |plan|
      plan.gives 100
      plan.every :month
      plan.signup_bonus 500
      plan.trial_includes 100
      plan.unused_credits :rollover
      plan.expire_after 7.days
      plan.meta(tier: "premium")
    end