usage_credits 0.1.1 → 0.2.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 +4 -4
- data/.simplecov +48 -0
- data/AGENTS.md +5 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +5 -0
- data/README.md +77 -6
- data/gemfiles/pay_10.0.gemfile +29 -0
- data/gemfiles/pay_11.0.gemfile +29 -0
- data/gemfiles/pay_8.3.gemfile +29 -0
- data/gemfiles/pay_9.0.gemfile +29 -0
- data/lib/generators/usage_credits/templates/initializer.rb +30 -2
- data/lib/usage_credits/configuration.rb +50 -3
- data/lib/usage_credits/helpers/period_parser.rb +43 -5
- data/lib/usage_credits/models/allocation.rb +2 -0
- data/lib/usage_credits/models/concerns/pay_charge_extension.rb +92 -44
- data/lib/usage_credits/models/concerns/pay_subscription_extension.rb +376 -33
- data/lib/usage_credits/models/credit_subscription_plan.rb +115 -14
- data/lib/usage_credits/models/transaction.rb +1 -0
- data/lib/usage_credits/models/wallet.rb +15 -10
- data/lib/usage_credits/services/fulfillment_service.rb +15 -6
- data/lib/usage_credits/version.rb +1 -1
- metadata +119 -10
|
@@ -26,6 +26,7 @@ module UsageCredits
|
|
|
26
26
|
"subscription_credits", # Generic subscription credits
|
|
27
27
|
"subscription_trial", # Trial period credits
|
|
28
28
|
"subscription_signup_bonus", # Bonus for subscribing
|
|
29
|
+
"subscription_upgrade", # Plan upgrade credits
|
|
29
30
|
|
|
30
31
|
# One-time purchases
|
|
31
32
|
"credit_pack", # Generic credit pack
|
|
@@ -94,9 +94,10 @@ module UsageCredits
|
|
|
94
94
|
raise InsufficientCredits, "Insufficient credits (#{credits} < #{cost})" unless has_enough_credits_to?(operation_name, **params)
|
|
95
95
|
|
|
96
96
|
# Create audit trail
|
|
97
|
-
audit_data
|
|
97
|
+
# Stringify keys from audit_data to avoid duplicate key warnings in JSON
|
|
98
|
+
audit_data = operation.to_audit_hash(params).deep_stringify_keys
|
|
98
99
|
deduct_params = {
|
|
99
|
-
metadata: audit_data.merge(operation.metadata).merge(
|
|
100
|
+
metadata: audit_data.merge(operation.metadata.deep_stringify_keys).merge(
|
|
100
101
|
"executed_at" => Time.current,
|
|
101
102
|
"gem_version" => UsageCredits::VERSION
|
|
102
103
|
),
|
|
@@ -126,7 +127,7 @@ module UsageCredits
|
|
|
126
127
|
def give_credits(amount, reason: nil, expires_at: nil)
|
|
127
128
|
raise ArgumentError, "Amount is required" if amount.nil?
|
|
128
129
|
raise ArgumentError, "Cannot give negative credits" if amount.to_i.negative?
|
|
129
|
-
raise ArgumentError, "Credit amount must be a whole number" unless amount.to_i
|
|
130
|
+
raise ArgumentError, "Credit amount must be a whole number" unless amount == amount.to_i
|
|
130
131
|
raise ArgumentError, "Expiration date must be a valid datetime" if expires_at && !expires_at.respond_to?(:to_datetime)
|
|
131
132
|
raise ArgumentError, "Expiration date must be in the future" if expires_at && expires_at <= Time.current
|
|
132
133
|
|
|
@@ -155,8 +156,6 @@ module UsageCredits
|
|
|
155
156
|
amount = amount.to_i
|
|
156
157
|
raise ArgumentError, "Cannot add non-positive credits" if amount <= 0
|
|
157
158
|
|
|
158
|
-
previous_balance = credits
|
|
159
|
-
|
|
160
159
|
transaction = transactions.create!(
|
|
161
160
|
amount: amount,
|
|
162
161
|
category: category,
|
|
@@ -170,7 +169,6 @@ module UsageCredits
|
|
|
170
169
|
save!
|
|
171
170
|
|
|
172
171
|
notify_balance_change(:credits_added, amount)
|
|
173
|
-
check_low_balance if !was_low_balance?(previous_balance) && low_balance?
|
|
174
172
|
|
|
175
173
|
# To finish, let's return the transaction that has been just created so we can reference it in parts of the code
|
|
176
174
|
# Useful, for example, to update the transaction's `fulfillment` reference in the subscription extension
|
|
@@ -186,15 +184,18 @@ module UsageCredits
|
|
|
186
184
|
# positive transactions still have leftover.
|
|
187
185
|
#
|
|
188
186
|
# TODO: This code enumerates all unexpired positive transactions each time.
|
|
189
|
-
# That
|
|
187
|
+
# That's fine if usage scale is moderate. We're already indexing this.
|
|
190
188
|
# If performance becomes a concern, we need to create a separate model to store the partial allocations efficiently.
|
|
191
189
|
def deduct_credits(amount, metadata: {}, category: :credit_deducted)
|
|
192
|
-
|
|
190
|
+
with_lock do
|
|
193
191
|
amount = amount.to_i
|
|
194
192
|
raise InsufficientCredits, "Cannot deduct a non-positive amount" if amount <= 0
|
|
195
193
|
|
|
194
|
+
# Capture previous balance for low_balance check
|
|
195
|
+
previous_balance = credits
|
|
196
|
+
|
|
196
197
|
# Figure out how many credits are available right now
|
|
197
|
-
available =
|
|
198
|
+
available = previous_balance
|
|
198
199
|
if amount > available && !allow_negative_balance?
|
|
199
200
|
raise InsufficientCredits, "Insufficient credits (#{available} < #{amount})"
|
|
200
201
|
end
|
|
@@ -254,9 +255,13 @@ module UsageCredits
|
|
|
254
255
|
|
|
255
256
|
# Fire your existing notifications
|
|
256
257
|
notify_balance_change(:credits_deducted, amount)
|
|
258
|
+
|
|
259
|
+
# Check if we crossed the low balance threshold
|
|
260
|
+
check_low_balance if !was_low_balance?(previous_balance) && low_balance?
|
|
261
|
+
|
|
257
262
|
spend_tx
|
|
263
|
+
end
|
|
258
264
|
end
|
|
259
|
-
end
|
|
260
265
|
|
|
261
266
|
|
|
262
267
|
private
|
|
@@ -100,7 +100,15 @@ module UsageCredits
|
|
|
100
100
|
return nil unless @fulfillment.fulfillment_type == "subscription" && @plan
|
|
101
101
|
return nil if @plan.rollover_enabled
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
# Cap the grace period to the fulfillment period to prevent balance accumulation
|
|
104
|
+
# when fulfillment_period << grace_period (e.g., 15 seconds vs 5 minutes)
|
|
105
|
+
fulfillment_period = @plan.parsed_fulfillment_period
|
|
106
|
+
effective_grace = [
|
|
107
|
+
UsageCredits.configuration.fulfillment_grace_period,
|
|
108
|
+
fulfillment_period
|
|
109
|
+
].min
|
|
110
|
+
|
|
111
|
+
@fulfillment.calculate_next_fulfillment + effective_grace
|
|
104
112
|
end
|
|
105
113
|
|
|
106
114
|
def fulfillment_category
|
|
@@ -112,15 +120,16 @@ module UsageCredits
|
|
|
112
120
|
end
|
|
113
121
|
|
|
114
122
|
def fulfillment_metadata
|
|
123
|
+
# Use string keys consistently to avoid duplicates after JSON serialization
|
|
115
124
|
base_metadata = {
|
|
116
|
-
last_fulfilled_at
|
|
117
|
-
reason
|
|
118
|
-
fulfillment_period
|
|
119
|
-
fulfillment_id
|
|
125
|
+
"last_fulfilled_at" => Time.current,
|
|
126
|
+
"reason" => "fulfillment_cycle",
|
|
127
|
+
"fulfillment_period" => @fulfillment.fulfillment_period,
|
|
128
|
+
"fulfillment_id" => @fulfillment.id
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
if @fulfillment.source.is_a?(Pay::Subscription)
|
|
123
|
-
base_metadata[
|
|
132
|
+
base_metadata["subscription_id"] = @fulfillment.source.id
|
|
124
133
|
end
|
|
125
134
|
|
|
126
135
|
@fulfillment.metadata.merge(base_metadata)
|
metadata
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: usage_credits
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-29 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pay
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.3'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '12.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.3'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '12.0'
|
|
13
32
|
- !ruby/object:Gem::Dependency
|
|
14
33
|
name: rails
|
|
15
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -25,19 +44,103 @@ dependencies:
|
|
|
25
44
|
- !ruby/object:Gem::Version
|
|
26
45
|
version: '6.1'
|
|
27
46
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
47
|
+
name: bundler
|
|
29
48
|
requirement: !ruby/object:Gem::Requirement
|
|
30
49
|
requirements:
|
|
31
50
|
- - "~>"
|
|
32
51
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
34
|
-
type: :
|
|
52
|
+
version: '2.0'
|
|
53
|
+
type: :development
|
|
35
54
|
prerelease: false
|
|
36
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
56
|
requirements:
|
|
38
57
|
- - "~>"
|
|
39
58
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
59
|
+
version: '2.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rake
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '13.0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '13.0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: minitest
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '5.0'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '5.0'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: sqlite3
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '2.1'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '2.1'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: rubocop
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '1.0'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '1.0'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: rubocop-minitest
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0.35'
|
|
123
|
+
type: :development
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0.35'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: rubocop-performance
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '1.0'
|
|
137
|
+
type: :development
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '1.0'
|
|
41
144
|
description: Add a usage-based credit system to your Rails app, easily. Let users
|
|
42
145
|
buy and spend credits on usage-based actions. Refill Stripe subscriptions with credits,
|
|
43
146
|
sell one-time booster credit packs, implement PAYG (pay-as-you-go) billing, award
|
|
@@ -51,10 +154,18 @@ extensions: []
|
|
|
51
154
|
extra_rdoc_files: []
|
|
52
155
|
files:
|
|
53
156
|
- ".rubocop.yml"
|
|
157
|
+
- ".simplecov"
|
|
158
|
+
- AGENTS.md
|
|
159
|
+
- Appraisals
|
|
54
160
|
- CHANGELOG.md
|
|
161
|
+
- CLAUDE.md
|
|
55
162
|
- LICENSE.txt
|
|
56
163
|
- README.md
|
|
57
164
|
- Rakefile
|
|
165
|
+
- gemfiles/pay_10.0.gemfile
|
|
166
|
+
- gemfiles/pay_11.0.gemfile
|
|
167
|
+
- gemfiles/pay_8.3.gemfile
|
|
168
|
+
- gemfiles/pay_9.0.gemfile
|
|
58
169
|
- lib/generators/usage_credits/install_generator.rb
|
|
59
170
|
- lib/generators/usage_credits/templates/create_usage_credits_tables.rb.erb
|
|
60
171
|
- lib/generators/usage_credits/templates/initializer.rb
|
|
@@ -93,7 +204,6 @@ metadata:
|
|
|
93
204
|
source_code_uri: https://github.com/rameerez/usage_credits
|
|
94
205
|
changelog_uri: https://github.com/rameerez/usage_credits/blob/main/CHANGELOG.md
|
|
95
206
|
rubygems_mfa_required: 'true'
|
|
96
|
-
post_install_message:
|
|
97
207
|
rdoc_options: []
|
|
98
208
|
require_paths:
|
|
99
209
|
- lib
|
|
@@ -108,8 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
108
218
|
- !ruby/object:Gem::Version
|
|
109
219
|
version: '0'
|
|
110
220
|
requirements: []
|
|
111
|
-
rubygems_version: 3.
|
|
112
|
-
signing_key:
|
|
221
|
+
rubygems_version: 3.6.2
|
|
113
222
|
specification_version: 4
|
|
114
223
|
summary: Add usage-based credits to your Rails app.
|
|
115
224
|
test_files: []
|