ask-token-usage-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +53 -0
- data/lib/ask/token_usage/rails/concerns/has_token_wallet.rb +89 -0
- data/lib/ask/token_usage/rails/jobs/sweep_expired_tokens_job.rb +76 -0
- data/lib/ask/token_usage/rails/models/token_transaction.rb +37 -0
- data/lib/ask/token_usage/rails/models/token_wallet.rb +21 -0
- data/lib/ask/token_usage/rails/railtie.rb +22 -0
- data/lib/ask/token_usage/rails/stores/active_record_store.rb +94 -0
- data/lib/ask/token_usage/rails/version.rb +9 -0
- data/lib/ask-token-usage-rails.rb +5 -0
- data/lib/generators/ask_token_usage/install/install_generator.rb +31 -0
- data/lib/generators/ask_token_usage/install/templates/initializer.rb +15 -0
- data/lib/generators/ask_token_usage/install/templates/migration.rb +25 -0
- metadata +129 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 34b1d95794c074506226ba84ddbc01130d7ba026663751741d0c13d497949b55
|
|
4
|
+
data.tar.gz: 1d6764ef111ded783089f8703182dede3b0dbeadf8b3be5e106179c6f797efc6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d4f4d4219eb29625d4b962c981da5677216a150c972f27b73fc90eafbb64526e12abb6302883c514e50d039654faf60b26d81127e44fbd044d71c828c3f3ed79
|
|
7
|
+
data.tar.gz: 9a55d29615156f17d6baef130f7607c20d4cce8d49f843c97bbddeff869f71fc7c14c712b25decfadc4f7ea54e149539a8b66f3ad8e2de470925730461bb046e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `has_token_wallet` concern for any ActiveRecord model.
|
|
7
|
+
- `Ask::TokenUsage::TokenWallet` (polymorphic owner + cached balance).
|
|
8
|
+
- `Ask::TokenUsage::TokenTransaction` append-only ledger (immutable).
|
|
9
|
+
- `Ask::TokenUsage::Rails::ActiveRecordStore` implementing the core Store port.
|
|
10
|
+
- `rails g ask_token_usage:install` — migration (`token_wallets`, `token_transactions`) + initializer.
|
|
11
|
+
- `SweepExpiredTokensJob` for expiring past-due grant entries.
|
|
12
|
+
- Automatic store swap via Railtie — no manual wiring required.
|
|
13
|
+
- Standard ask-gem infrastructure: CI, rubocop, overcommit.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaka Ruto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ask-token-usage-rails
|
|
2
|
+
|
|
3
|
+
ActiveRecord persistence for the [ask-token-usage](https://github.com/ask-rb/ask-token-usage) wallet engine.
|
|
4
|
+
|
|
5
|
+
Ships an ActiveRecord store adapter, a `has_token_wallet` concern, install generator with migrations, and an expiry sweep job.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem "ask-token-usage-rails"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bundle install
|
|
16
|
+
rails g ask_token_usage:install
|
|
17
|
+
rails db:migrate
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class User < ApplicationRecord
|
|
24
|
+
has_token_wallet
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Grant tokens
|
|
28
|
+
user.grant_tokens!(10_000, reason: :trial, expires_at: 7.days.from_now)
|
|
29
|
+
|
|
30
|
+
# Spend on an activity (charges only if the block succeeds)
|
|
31
|
+
user.spend_tokens_on!(:chat_message, input: "hi", output: "yo") do
|
|
32
|
+
LLM.chat(...)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check balance
|
|
36
|
+
user.token_balance # => 9_998
|
|
37
|
+
|
|
38
|
+
# Use the PORO wallet directly
|
|
39
|
+
wallet = user.ask_token_wallet
|
|
40
|
+
wallet.entries
|
|
41
|
+
wallet.used_since(30.days.ago)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Scheduled jobs
|
|
45
|
+
|
|
46
|
+
`SweepExpiredTokensJob` expires grant entries whose `expires_at` has passed.
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# config/recurring.yml (Solid Queue)
|
|
50
|
+
ask_token_usage_sweep:
|
|
51
|
+
class: Ask::TokenUsage::Rails::SweepExpiredTokensJob
|
|
52
|
+
schedule: "every 1 hour"
|
|
53
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module TokenUsage
|
|
7
|
+
module Rails
|
|
8
|
+
# Include this concern in any ActiveRecord model to give it a token
|
|
9
|
+
# wallet backed by ask-token-usage-rails.
|
|
10
|
+
#
|
|
11
|
+
# class User < ApplicationRecord
|
|
12
|
+
# has_token_wallet
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Adds:
|
|
16
|
+
# user.token_balance # Integer
|
|
17
|
+
# user.token_wallet # Ask::TokenUsage::TokenWallet (lazy-created)
|
|
18
|
+
# user.token_transactions # AR scope for the ledger
|
|
19
|
+
# user.grant_tokens!(...) # convenience
|
|
20
|
+
# user.spend_tokens!(...) # convenience
|
|
21
|
+
# user.spend_tokens_on!(...)# convenience (activity name + block)
|
|
22
|
+
#
|
|
23
|
+
module HasTokenWallet
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
|
|
26
|
+
included do
|
|
27
|
+
has_one :token_wallet, as: :owner,
|
|
28
|
+
class_name: "Ask::TokenUsage::TokenWallet",
|
|
29
|
+
dependent: :destroy,
|
|
30
|
+
inverse_of: :owner
|
|
31
|
+
|
|
32
|
+
has_many :token_transactions, through: :token_wallet
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ensure_token_wallet!
|
|
36
|
+
# Force wallet row creation by probing balance through the AR store
|
|
37
|
+
Ask::TokenUsage.wallet_for(self).balance
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def token_balance
|
|
41
|
+
Ask::TokenUsage.wallet_for(self).balance
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Build an Ask::TokenUsage::Wallet PORO backed by this model's
|
|
45
|
+
# AR wallet — use for one-off operations outside the concern API.
|
|
46
|
+
def ask_token_wallet
|
|
47
|
+
Ask::TokenUsage.wallet_for(self)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Grant +amount+ tokens.
|
|
51
|
+
def grant_tokens!(amount, reason:, expires_at: nil, metadata: {})
|
|
52
|
+
ask_token_wallet.grant!(amount, reason: reason, expires_at: expires_at, metadata: metadata)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Deduct +amount+ tokens, raising InsufficientTokens on failure.
|
|
56
|
+
def deduct_tokens!(amount, reason:, metadata: {})
|
|
57
|
+
ask_token_wallet.deduct!(amount, reason: reason, metadata: metadata)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Deduct +amount+ tokens; returns false instead of raising on
|
|
61
|
+
# insufficient balance.
|
|
62
|
+
def try_deduct_tokens!(amount, reason:, metadata: {})
|
|
63
|
+
deduct_tokens!(amount, reason: reason, metadata: metadata)
|
|
64
|
+
rescue Ask::TokenUsage::InsufficientTokens
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Spend the estimated cost of +activity+ with +params+, charging
|
|
69
|
+
# only when the block succeeds.
|
|
70
|
+
def spend_tokens_on!(activity_name, params = {}, &block)
|
|
71
|
+
ask_token_wallet.spend!(activity_name, params, &block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Spend an explicit +amount+ of tokens, charging only on block success.
|
|
75
|
+
def spend_tokens!(amount, reason:, metadata: {}, &block)
|
|
76
|
+
ask_token_wallet.deduct!(amount, reason: reason, metadata: metadata, &block)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def has_tokens_for?(amount)
|
|
80
|
+
ask_token_wallet.has?(amount)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def token_usage_since(time)
|
|
84
|
+
ask_token_wallet.used_since(time)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module TokenUsage
|
|
7
|
+
module Rails
|
|
8
|
+
# Expire grant entries whose +expires_at+ has passed. Scheduled as a
|
|
9
|
+
# recurring job (Solid Queue cron, Sidekiq Cron, etc.):
|
|
10
|
+
#
|
|
11
|
+
# # config/recurring.yml
|
|
12
|
+
# ask_token_usage_sweep:
|
|
13
|
+
# class: Ask::TokenUsage::Rails::SweepExpiredTokensJob
|
|
14
|
+
# schedule: "every 1 hour"
|
|
15
|
+
#
|
|
16
|
+
# For each expired grant, the actual amount removed is capped to the
|
|
17
|
+
# wallet's current balance (myrr approximation — correct for most
|
|
18
|
+
# grant-heavy usage patterns; perfect accuracy requires FIFO allocation
|
|
19
|
+
# which is a future option).
|
|
20
|
+
#
|
|
21
|
+
class SweepExpiredTokensJob < ActiveJob::Base
|
|
22
|
+
queue_as :default
|
|
23
|
+
|
|
24
|
+
def perform(now: Time.current)
|
|
25
|
+
Ask::TokenUsage::TokenTransaction
|
|
26
|
+
.grants
|
|
27
|
+
.where("expires_at IS NOT NULL AND expires_at <= ?", now)
|
|
28
|
+
.where.not(entry_type: "expiry")
|
|
29
|
+
.where("id NOT IN (SELECT source_transaction_id FROM token_transactions WHERE entry_type = ?)", "expiry")
|
|
30
|
+
.find_each do |grant|
|
|
31
|
+
sweep_grant(grant)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def sweep_grant(grant)
|
|
38
|
+
wallet = grant.token_wallet
|
|
39
|
+
return unless wallet
|
|
40
|
+
|
|
41
|
+
wallet.with_lock do
|
|
42
|
+
remaining = grant.amount - already_expired_amount(grant)
|
|
43
|
+
return if remaining <= 0
|
|
44
|
+
|
|
45
|
+
actual = [remaining, wallet.balance].min
|
|
46
|
+
return if actual <= 0
|
|
47
|
+
|
|
48
|
+
Ask::TokenUsage::TokenTransaction.create!(
|
|
49
|
+
token_wallet_id: wallet.id,
|
|
50
|
+
entry_type: "expiry",
|
|
51
|
+
amount: -actual,
|
|
52
|
+
reason: "token_expiry",
|
|
53
|
+
balance: wallet.balance - actual,
|
|
54
|
+
metadata: {
|
|
55
|
+
source_transaction_id: grant.id,
|
|
56
|
+
expired_amount: remaining,
|
|
57
|
+
actual_expired: actual
|
|
58
|
+
},
|
|
59
|
+
created_at: now
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
wallet.update_column(:balance, wallet.balance - actual)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def already_expired_amount(grant)
|
|
67
|
+
Ask::TokenUsage::TokenTransaction
|
|
68
|
+
.where(token_wallet_id: grant.token_wallet_id, entry_type: "expiry")
|
|
69
|
+
.where("metadata->>'source_transaction_id' = ?", grant.id.to_s)
|
|
70
|
+
.sum(:amount)
|
|
71
|
+
.abs
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module TokenUsage
|
|
7
|
+
# Append-only ledger row. Every grant, debit, adjustment, or expiry is
|
|
8
|
+
# written here as an immutable record. Never updated or deleted.
|
|
9
|
+
class TokenTransaction < ::ActiveRecord::Base
|
|
10
|
+
self.table_name = "token_transactions"
|
|
11
|
+
|
|
12
|
+
belongs_to :token_wallet, class_name: "Ask::TokenUsage::TokenWallet",
|
|
13
|
+
foreign_key: :token_wallet_id,
|
|
14
|
+
inverse_of: :token_transactions
|
|
15
|
+
|
|
16
|
+
ENTRY_TYPES = %w[grant debit adjustment expiry].freeze
|
|
17
|
+
validates :entry_type, presence: true, inclusion: { in: ENTRY_TYPES }
|
|
18
|
+
validates :amount, presence: true, numericality: { only_integer: true }
|
|
19
|
+
validates :reason, presence: true
|
|
20
|
+
|
|
21
|
+
scope :grants, -> { where(entry_type: "grant") }
|
|
22
|
+
scope :debits, -> { where(entry_type: "debit") }
|
|
23
|
+
scope :adjustments, -> { where(entry_type: "adjustment") }
|
|
24
|
+
scope :expiries, -> { where(entry_type: "expiry") }
|
|
25
|
+
scope :since, ->(time) { where("created_at >= ?", time) }
|
|
26
|
+
scope :newest_first, -> { order(created_at: :desc) }
|
|
27
|
+
|
|
28
|
+
validate :immutable_after_creation, on: :update
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def immutable_after_creation
|
|
33
|
+
errors.add(:base, "Token transactions are append-only")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module TokenUsage
|
|
7
|
+
# Polymorphic wallet record. One per owner. Holds the cached balance.
|
|
8
|
+
class TokenWallet < ::ActiveRecord::Base
|
|
9
|
+
self.table_name = "token_wallets"
|
|
10
|
+
|
|
11
|
+
has_many :token_transactions, class_name: "Ask::TokenUsage::TokenTransaction",
|
|
12
|
+
foreign_key: :token_wallet_id,
|
|
13
|
+
dependent: :destroy,
|
|
14
|
+
inverse_of: :token_wallet
|
|
15
|
+
|
|
16
|
+
belongs_to :owner, polymorphic: true, optional: true
|
|
17
|
+
|
|
18
|
+
validates :balance, numericality: { only_integer: true }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module TokenUsage
|
|
7
|
+
module Rails
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
initializer "ask_token_usage.active_record_store" do
|
|
10
|
+
require "ask/token_usage/rails/models/token_wallet"
|
|
11
|
+
require "ask/token_usage/rails/models/token_transaction"
|
|
12
|
+
require "ask/token_usage/rails/stores/active_record_store"
|
|
13
|
+
require "ask/token_usage/rails/concerns/has_token_wallet"
|
|
14
|
+
|
|
15
|
+
Ask::TokenUsage.configure do |c|
|
|
16
|
+
c.store = Ask::TokenUsage::Rails::ActiveRecordStore.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ask/token_usage/stores/store"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module TokenUsage
|
|
7
|
+
module Rails
|
|
8
|
+
# ActiveRecord-backed wallet store. Each mutation (grant/deduct) does
|
|
9
|
+
# its own read-write under a single DB call. The wallet row is created
|
|
10
|
+
# lazily on first access. No outer transaction wrapping — each AR
|
|
11
|
+
# create/update handles its own atomicity.
|
|
12
|
+
class ActiveRecordStore < Ask::TokenUsage::Stores::Store
|
|
13
|
+
def balance(owner)
|
|
14
|
+
resolve_wallet(owner)&.balance || 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def entries(owner, kind: nil, since: nil)
|
|
18
|
+
row = resolve_wallet(owner)
|
|
19
|
+
return [] unless row
|
|
20
|
+
|
|
21
|
+
scope = row.token_transactions.order(:created_at)
|
|
22
|
+
scope = scope.where(entry_type: kind.to_s) if kind
|
|
23
|
+
scope = scope.where("created_at >= ?", since) if since
|
|
24
|
+
scope.map { |t| to_entry(t) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# No-op for in-process locking. For production with PostgreSQL,
|
|
28
|
+
# this could use row-level locks. SQLite uses file-level locks
|
|
29
|
+
# which are handled by the OS.
|
|
30
|
+
def with_lock(owner)
|
|
31
|
+
yield
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def append(owner, entry)
|
|
35
|
+
row = find_or_create_wallet!(owner)
|
|
36
|
+
txn = Ask::TokenUsage::TokenTransaction.create!(
|
|
37
|
+
token_wallet_id: row.id,
|
|
38
|
+
entry_type: entry.kind.to_s,
|
|
39
|
+
amount: entry.amount,
|
|
40
|
+
reason: entry.reason,
|
|
41
|
+
metadata: entry.metadata || {},
|
|
42
|
+
expires_at: entry.expires_at,
|
|
43
|
+
balance: entry.balance,
|
|
44
|
+
created_at: entry.created_at
|
|
45
|
+
)
|
|
46
|
+
entry.with(id: txn.id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def write_balance(owner, amount)
|
|
50
|
+
row = find_or_create_wallet!(owner)
|
|
51
|
+
row.update_column(:balance, amount)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def find_or_create_wallet!(owner)
|
|
57
|
+
owner_type, owner_id = resolve_owner(owner)
|
|
58
|
+
Ask::TokenUsage::TokenWallet.find_by(owner_type: owner_type, owner_id: owner_id) ||
|
|
59
|
+
begin
|
|
60
|
+
Ask::TokenUsage::TokenWallet.create!(owner_type: owner_type, owner_id: owner_id, balance: 0)
|
|
61
|
+
rescue ActiveRecord::RecordNotUnique
|
|
62
|
+
Ask::TokenUsage::TokenWallet.find_by!(owner_type: owner_type, owner_id: owner_id)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def resolve_wallet(owner)
|
|
67
|
+
owner_type, owner_id = resolve_owner(owner)
|
|
68
|
+
Ask::TokenUsage::TokenWallet.find_by(owner_type: owner_type, owner_id: owner_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resolve_owner(owner)
|
|
72
|
+
if owner.respond_to?(:id) && owner.respond_to?(:class) && owner.class.respond_to?(:polymorphic_name)
|
|
73
|
+
[owner.class.polymorphic_name, owner.id]
|
|
74
|
+
else
|
|
75
|
+
[owner.class.name, owner.respond_to?(:id) ? owner.id : owner.to_s]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_entry(txn)
|
|
80
|
+
Ask::TokenUsage::LedgerEntry.new(
|
|
81
|
+
id: txn.id,
|
|
82
|
+
kind: txn.entry_type.to_sym,
|
|
83
|
+
amount: txn.amount,
|
|
84
|
+
reason: txn.reason,
|
|
85
|
+
expires_at: txn.expires_at,
|
|
86
|
+
metadata: txn.metadata,
|
|
87
|
+
balance: txn.balance,
|
|
88
|
+
created_at: txn.created_at
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module AskTokenUsage
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
|
|
12
|
+
desc "Creates the ask-token-usage migration and initializer"
|
|
13
|
+
|
|
14
|
+
def self.next_migration_number(_dir)
|
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_migration
|
|
19
|
+
migration_template "migration.rb", "db/migrate/create_token_wallets.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_initializer
|
|
23
|
+
template "initializer.rb", "config/initializers/ask_token_usage.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show_readme
|
|
27
|
+
readme "README.md" if behavior == :invoke
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ask-token-usage configuration.
|
|
4
|
+
# The railtie auto-wires the ActiveRecord store. Override settings here.
|
|
5
|
+
|
|
6
|
+
Ask::TokenUsage.configure do |config|
|
|
7
|
+
# What 1M billing tokens cost (the rate your users pay).
|
|
8
|
+
# config.price_per_1m = Money.from_amount(100, "USD") # $100 / 1M
|
|
9
|
+
|
|
10
|
+
# Rounding strategy for dynamic token costs: :ceil (default), :floor, :round.
|
|
11
|
+
# config.rounding = :ceil
|
|
12
|
+
|
|
13
|
+
# Allow negative balances? Default: false.
|
|
14
|
+
# config.negatives = false
|
|
15
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTokenWallets < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :token_wallets do |t|
|
|
6
|
+
t.references :owner, polymorphic: true, null: false
|
|
7
|
+
t.bigint :balance, null: false, default: 0
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :token_wallets, %i[owner_type owner_id], unique: true, name: "idx_token_wallets_owner"
|
|
11
|
+
|
|
12
|
+
create_table :token_transactions do |t|
|
|
13
|
+
t.references :token_wallet, null: false, foreign_key: true
|
|
14
|
+
t.string :entry_type, null: false
|
|
15
|
+
t.bigint :amount, null: false
|
|
16
|
+
t.string :reason, null: false
|
|
17
|
+
t.jsonb :metadata, null: false, default: {}
|
|
18
|
+
t.bigint :balance, null: false
|
|
19
|
+
t.datetime :expires_at
|
|
20
|
+
t.datetime :created_at, null: false
|
|
21
|
+
end
|
|
22
|
+
add_index :token_transactions, %i[token_wallet_id created_at]
|
|
23
|
+
add_index :token_transactions, :entry_type
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ask-token-usage-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kaka Ruto
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ask-token-usage
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.25'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.25'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: sqlite3
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: Adds an ActiveRecord-backed wallet store, has_token_wallet concern, token
|
|
83
|
+
wallet/transaction models, an install generator with migrations, and an expiry sweep
|
|
84
|
+
job to any Rails app using ask-token-usage.
|
|
85
|
+
email:
|
|
86
|
+
- kaka@myrrlabs.com
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- CHANGELOG.md
|
|
92
|
+
- LICENSE
|
|
93
|
+
- README.md
|
|
94
|
+
- lib/ask-token-usage-rails.rb
|
|
95
|
+
- lib/ask/token_usage/rails/concerns/has_token_wallet.rb
|
|
96
|
+
- lib/ask/token_usage/rails/jobs/sweep_expired_tokens_job.rb
|
|
97
|
+
- lib/ask/token_usage/rails/models/token_transaction.rb
|
|
98
|
+
- lib/ask/token_usage/rails/models/token_wallet.rb
|
|
99
|
+
- lib/ask/token_usage/rails/railtie.rb
|
|
100
|
+
- lib/ask/token_usage/rails/stores/active_record_store.rb
|
|
101
|
+
- lib/ask/token_usage/rails/version.rb
|
|
102
|
+
- lib/generators/ask_token_usage/install/install_generator.rb
|
|
103
|
+
- lib/generators/ask_token_usage/install/templates/initializer.rb
|
|
104
|
+
- lib/generators/ask_token_usage/install/templates/migration.rb
|
|
105
|
+
homepage: https://github.com/ask-rb/ask-token-usage-rails
|
|
106
|
+
licenses:
|
|
107
|
+
- MIT
|
|
108
|
+
metadata:
|
|
109
|
+
homepage_uri: https://github.com/ask-rb/ask-token-usage-rails
|
|
110
|
+
source_code_uri: https://github.com/ask-rb/ask-token-usage-rails
|
|
111
|
+
changelog_uri: https://github.com/ask-rb/ask-token-usage-rails/blob/main/CHANGELOG.md
|
|
112
|
+
rdoc_options: []
|
|
113
|
+
require_paths:
|
|
114
|
+
- lib
|
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '3.2'
|
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
requirements: []
|
|
126
|
+
rubygems_version: 4.0.18
|
|
127
|
+
specification_version: 4
|
|
128
|
+
summary: ActiveRecord persistence for the ask-token-usage wallet engine
|
|
129
|
+
test_files: []
|