Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,35 @@ This makes it easy to:
- Generate detailed invoices
- Monitor usage patterns

### Running balance (balance after each transaction)

Every transaction automatically tracks the wallet balance before and after it was applied, like you would find in a bank statement:

```ruby
user.credit_history.each do |tx|
puts "#{tx.created_at.strftime('%Y-%m-%d')}: #{tx.formatted_amount}"
puts " Balance: #{tx.balance_before} → #{tx.balance_after}"
end

# Output:
# 2024-12-16: +1000 credits
# Balance: 0 → 1000
# 2024-12-26: +500 credits
# Balance: 1000 → 1500
# 2025-01-14: -50 credits
# Balance: 1500 → 1450
```

This is useful for building transaction history UIs, generating statements, or debugging balance issues. Each transaction provides:

```ruby
transaction.balance_before # Balance before this transaction
transaction.balance_after # Balance after this transaction
transaction.formatted_balance_after # Formatted (e.g., "1450 credits")
```

`balance_before` and `balance_after` return `nil` if no balance is found (for transactions created before this feature was added)

### Custom credit formatting

A minor thing, but if you want to use the `@transaction.formatted_amount` helper, you can specify the format:
Expand Down
24 changes: 24 additions & 0 deletions lib/usage_credits/models/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ def remaining_amount
amount - allocated_amount
end

# =========================================
# Balance After Transaction
# =========================================

# Get the balance after this transaction was applied
# Returns nil for transactions created before this feature was added
def balance_after
metadata[:balance_after]
end

# Get the balance before this transaction was applied
# Returns the stored value if available, otherwise nil
# Note: For transactions created before this feature, returns nil
def balance_before
metadata[:balance_before]
end

# =========================================
# Display Formatting
# =========================================
Expand All @@ -126,6 +143,13 @@ def formatted_amount
"#{prefix}#{UsageCredits.configuration.credit_formatter.call(amount)}"
end

# Format the balance after for display (e.g., "500 credits")
# Returns nil if balance_after is not stored
def formatted_balance_after
return nil unless balance_after
UsageCredits.configuration.credit_formatter.call(balance_after)
end

# Get a human-readable description of what this transaction represents
def description
# Custom description takes precedence
Expand Down
20 changes: 20 additions & 0 deletions lib/usage_credits/models/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ def add_credits(amount, metadata: {}, category: :credit_added, expires_at: nil,
self.balance = credits
save!

# Store balance information in transaction metadata for audit trail.
# Note: This update! is in the same DB transaction as the create! above (via with_lock),
# so if this fails, the entire transaction rolls back - no orphaned records possible.
# We intentionally overwrite any user-supplied balance_before/balance_after keys
# to ensure system-set values are authoritative.
transaction.update!(metadata: transaction.metadata.merge(
balance_before: previous_balance,
balance_after: balance
))

# Dispatch callback with full context
UsageCredits::Callbacks.dispatch(:credits_added,
wallet: self,
Expand Down Expand Up @@ -277,6 +287,16 @@ def deduct_credits(amount, metadata: {}, category: :credit_deducted)
self.balance = credits
save!

# Store balance information in transaction metadata for audit trail.
# Note: This update! is in the same DB transaction as the create! above (via with_lock),
# so if this fails, the entire transaction rolls back - no orphaned records possible.
# We intentionally overwrite any user-supplied balance_before/balance_after keys
# to ensure system-set values are authoritative.
spend_tx.update!(metadata: spend_tx.metadata.merge(
balance_before: previous_balance,
balance_after: balance
))

# Dispatch credits_deducted callback
UsageCredits::Callbacks.dispatch(:credits_deducted,
wallet: self,
Expand Down
Loading
Loading