usage_credits 0.5.0 → 1.0.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/CHANGELOG.md +68 -0
- data/README.md +120 -29
- data/lib/generators/usage_credits/install_generator.rb +9 -1
- data/lib/generators/usage_credits/templates/create_usage_credits_tables.rb.erb +64 -20
- data/lib/generators/usage_credits/templates/upgrade_usage_credits_to_wallets_core.rb.erb +602 -0
- data/lib/generators/usage_credits/upgrade_generator.rb +47 -0
- data/lib/usage_credits/callbacks.rb +9 -54
- data/lib/usage_credits/configuration.rb +25 -13
- data/lib/usage_credits/core_ext/numeric.rb +12 -16
- data/lib/usage_credits/cost/base.rb +1 -6
- data/lib/usage_credits/cost/compound.rb +3 -2
- data/lib/usage_credits/cost/fixed.rb +1 -2
- data/lib/usage_credits/cost/variable.rb +23 -8
- data/lib/usage_credits/engine.rb +8 -12
- data/lib/usage_credits/helpers/credit_calculator.rb +18 -0
- data/lib/usage_credits/helpers/credits_helper.rb +9 -11
- data/lib/usage_credits/helpers/period_parser.rb +31 -9
- data/lib/usage_credits/helpers/processor_metadata.rb +46 -0
- data/lib/usage_credits/jobs/fulfillment_job.rb +1 -1
- data/lib/usage_credits/models/allocation.rb +15 -20
- data/lib/usage_credits/models/concerns/has_wallet.rb +50 -27
- data/lib/usage_credits/models/concerns/pay_charge_extension.rb +184 -139
- data/lib/usage_credits/models/concerns/pay_subscription_extension.rb +490 -174
- data/lib/usage_credits/models/credit_pack.rb +34 -13
- data/lib/usage_credits/models/credit_subscription_plan.rb +36 -15
- data/lib/usage_credits/models/fulfillment.rb +23 -48
- data/lib/usage_credits/models/operation.rb +36 -28
- data/lib/usage_credits/models/transaction.rb +51 -158
- data/lib/usage_credits/models/transfer.rb +37 -0
- data/lib/usage_credits/models/wallet.rb +219 -314
- data/lib/usage_credits/railtie.rb +4 -13
- data/lib/usage_credits/services/fulfillment_service.rb +146 -22
- data/lib/usage_credits/subscription_terms.rb +131 -0
- data/lib/usage_credits/version.rb +1 -1
- data/lib/usage_credits.rb +16 -3
- metadata +33 -20
- data/.rubocop.yml +0 -8
- data/.simplecov +0 -37
- data/AGENTS.md +0 -5
- data/Appraisals +0 -43
- data/CLAUDE.md +0 -5
- data/Rakefile +0 -32
- data/gemfiles/pay_10.0.gemfile +0 -41
- data/gemfiles/pay_11.0.gemfile +0 -41
- data/gemfiles/pay_8.3.gemfile +0 -41
- data/gemfiles/pay_9.0.gemfile +0 -41
- data/gemfiles/rails_7.2.gemfile +0 -41
- data/gemfiles/rails_8.1.gemfile +0 -41
|
@@ -3,146 +3,88 @@
|
|
|
3
3
|
module UsageCredits
|
|
4
4
|
# Records all credit changes in a wallet (additions, deductions, expirations).
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# -
|
|
8
|
-
# -
|
|
9
|
-
# -
|
|
10
|
-
|
|
11
|
-
class Transaction <
|
|
12
|
-
|
|
6
|
+
# This class extends Wallets::Transaction with usage_credits-specific features:
|
|
7
|
+
# - Fulfillment tracking for subscription/pack credits
|
|
8
|
+
# - Usage-credits specific transaction categories
|
|
9
|
+
# - Operation charge descriptions and formatting
|
|
10
|
+
|
|
11
|
+
class Transaction < Wallets::TransactionBase
|
|
12
|
+
# =========================================
|
|
13
|
+
# Embeddability Configuration
|
|
14
|
+
# =========================================
|
|
15
|
+
|
|
16
|
+
self.embedded_table_name = "usage_credits_transactions"
|
|
17
|
+
self.config_provider = -> { UsageCredits.configuration }
|
|
13
18
|
|
|
14
19
|
# =========================================
|
|
15
20
|
# Transaction Categories
|
|
16
21
|
# =========================================
|
|
17
22
|
|
|
18
|
-
#
|
|
23
|
+
# Override base categories with usage_credits-specific ones
|
|
19
24
|
DEFAULT_CATEGORIES = [
|
|
20
25
|
# Bonus credits
|
|
21
|
-
"signup_bonus",
|
|
22
|
-
"referral_bonus",
|
|
23
|
-
"bonus",
|
|
26
|
+
"signup_bonus",
|
|
27
|
+
"referral_bonus",
|
|
28
|
+
"bonus",
|
|
24
29
|
|
|
25
30
|
# Subscription-related
|
|
26
|
-
"subscription_credits",
|
|
27
|
-
"subscription_trial",
|
|
28
|
-
"subscription_signup_bonus",
|
|
29
|
-
"subscription_upgrade",
|
|
31
|
+
"subscription_credits",
|
|
32
|
+
"subscription_trial",
|
|
33
|
+
"subscription_signup_bonus",
|
|
34
|
+
"subscription_upgrade",
|
|
30
35
|
|
|
31
36
|
# One-time purchases
|
|
32
|
-
"credit_pack",
|
|
33
|
-
"credit_pack_purchase",
|
|
34
|
-
"credit_pack_refund",
|
|
37
|
+
"credit_pack",
|
|
38
|
+
"credit_pack_purchase",
|
|
39
|
+
"credit_pack_refund",
|
|
35
40
|
|
|
36
41
|
# Credit usage & management
|
|
37
|
-
"operation_charge",
|
|
38
|
-
"manual_adjustment",
|
|
39
|
-
"credit_added",
|
|
40
|
-
"credit_deducted"
|
|
42
|
+
"operation_charge",
|
|
43
|
+
"manual_adjustment",
|
|
44
|
+
"credit_added",
|
|
45
|
+
"credit_deducted",
|
|
46
|
+
|
|
47
|
+
# Transfer categories (from wallets)
|
|
48
|
+
"transfer_in",
|
|
49
|
+
"transfer_out"
|
|
41
50
|
].freeze
|
|
42
51
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
52
|
+
# Kept as an immutable compatibility snapshot for callers that referenced
|
|
53
|
+
# the historical constant. Runtime validation uses .categories so configured
|
|
54
|
+
# additions remain dynamic without mutating this public constant.
|
|
55
|
+
CATEGORIES = DEFAULT_CATEGORIES
|
|
56
|
+
|
|
45
57
|
def self.categories
|
|
46
|
-
(DEFAULT_CATEGORIES +
|
|
58
|
+
(DEFAULT_CATEGORIES + resolved_config.additional_categories).uniq
|
|
47
59
|
end
|
|
48
60
|
|
|
49
|
-
# Backwards compatibility: CATEGORIES constant still works
|
|
50
|
-
# but prefer using Transaction.categories for dynamic lookup
|
|
51
|
-
CATEGORIES = DEFAULT_CATEGORIES
|
|
52
|
-
|
|
53
61
|
# =========================================
|
|
54
|
-
# Associations
|
|
62
|
+
# Additional Associations
|
|
55
63
|
# =========================================
|
|
56
64
|
|
|
57
|
-
belongs_to :wallet
|
|
58
|
-
|
|
59
|
-
belongs_to :fulfillment, optional: true
|
|
65
|
+
belongs_to :wallet, class_name: "UsageCredits::Wallet", inverse_of: :transactions, optional: false
|
|
66
|
+
belongs_to :transfer, class_name: "UsageCredits::Transfer", optional: true
|
|
67
|
+
belongs_to :fulfillment, class_name: "UsageCredits::Fulfillment", optional: true
|
|
60
68
|
|
|
69
|
+
# Re-declare allocation associations with correct classes
|
|
61
70
|
has_many :outgoing_allocations,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
class_name: "UsageCredits::Allocation",
|
|
72
|
+
foreign_key: :transaction_id,
|
|
73
|
+
dependent: :destroy
|
|
65
74
|
|
|
66
75
|
has_many :incoming_allocations,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
validates :amount, presence: true, numericality: { only_integer: true }
|
|
72
|
-
validates :category, presence: true, inclusion: { in: ->(record) { Transaction.categories } }
|
|
73
|
-
|
|
74
|
-
validate :remaining_amount_cannot_be_negative
|
|
76
|
+
class_name: "UsageCredits::Allocation",
|
|
77
|
+
foreign_key: :source_transaction_id,
|
|
78
|
+
dependent: :destroy
|
|
75
79
|
|
|
76
80
|
# =========================================
|
|
77
|
-
# Scopes
|
|
81
|
+
# Backwards Compatibility Scopes
|
|
78
82
|
# =========================================
|
|
79
83
|
|
|
80
|
-
scope :credits_added, -> { where(
|
|
81
|
-
scope :credits_deducted, -> { where(
|
|
82
|
-
scope :by_category, ->(category) { where(category: category) }
|
|
83
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
84
|
+
scope :credits_added, -> { where(arel_table[:amount].gt(0)) }
|
|
85
|
+
scope :credits_deducted, -> { where(arel_table[:amount].lt(0)) }
|
|
84
86
|
scope :operation_charges, -> { where(category: :operation_charge) }
|
|
85
87
|
|
|
86
|
-
# A transaction is not expired if:
|
|
87
|
-
# 1. It has no expiration date, OR
|
|
88
|
-
# 2. Its expiration date is in the future
|
|
89
|
-
scope :not_expired, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
90
|
-
scope :expired, -> { where("expires_at < ?", Time.current) }
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# =========================================
|
|
94
|
-
# Helpers
|
|
95
|
-
# =========================================
|
|
96
|
-
|
|
97
|
-
# Get the owner of the wallet these credits belong to
|
|
98
|
-
def owner
|
|
99
|
-
wallet.owner
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Have these credits expired?
|
|
103
|
-
def expired?
|
|
104
|
-
expires_at.present? && expires_at < Time.current
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Is this transaction a positive credit or a negative (spend)?
|
|
108
|
-
def credit?
|
|
109
|
-
amount > 0
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def debit?
|
|
113
|
-
amount < 0
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# How many credits from this transaction have already been allocated (spent)?
|
|
117
|
-
# Only applies if this transaction is positive.
|
|
118
|
-
def allocated_amount
|
|
119
|
-
incoming_allocations.sum(:amount)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# How many credits remain unused in this positive transaction?
|
|
123
|
-
# If negative, this will effectively be 0.
|
|
124
|
-
def remaining_amount
|
|
125
|
-
return 0 unless credit?
|
|
126
|
-
amount - allocated_amount
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# =========================================
|
|
130
|
-
# Balance After Transaction
|
|
131
|
-
# =========================================
|
|
132
|
-
|
|
133
|
-
# Get the balance after this transaction was applied
|
|
134
|
-
# Returns nil for transactions created before this feature was added
|
|
135
|
-
def balance_after
|
|
136
|
-
metadata[:balance_after]
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Get the balance before this transaction was applied
|
|
140
|
-
# Returns the stored value if available, otherwise nil
|
|
141
|
-
# Note: For transactions created before this feature, returns nil
|
|
142
|
-
def balance_before
|
|
143
|
-
metadata[:balance_before]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
88
|
# =========================================
|
|
147
89
|
# Display Formatting
|
|
148
90
|
# =========================================
|
|
@@ -154,7 +96,6 @@ module UsageCredits
|
|
|
154
96
|
end
|
|
155
97
|
|
|
156
98
|
# Format the balance after for display (e.g., "500 credits")
|
|
157
|
-
# Returns nil if balance_after is not stored
|
|
158
99
|
def formatted_balance_after
|
|
159
100
|
return nil unless balance_after
|
|
160
101
|
UsageCredits.configuration.credit_formatter.call(balance_after)
|
|
@@ -162,54 +103,13 @@ module UsageCredits
|
|
|
162
103
|
|
|
163
104
|
# Get a human-readable description of what this transaction represents
|
|
164
105
|
def description
|
|
165
|
-
# Custom description takes precedence
|
|
166
106
|
return self[:description] if self[:description].present?
|
|
167
|
-
|
|
168
|
-
# Operation charges have dynamic descriptions
|
|
169
107
|
return operation_description if category == "operation_charge"
|
|
170
|
-
|
|
171
|
-
# Use predefined description or fallback to titleized category
|
|
172
108
|
category.titleize
|
|
173
109
|
end
|
|
174
110
|
|
|
175
|
-
# =========================================
|
|
176
|
-
# Metadata Handling
|
|
177
|
-
# =========================================
|
|
178
|
-
|
|
179
|
-
# Sync in-place modifications to metadata before saving
|
|
180
|
-
before_save :sync_metadata_cache
|
|
181
|
-
|
|
182
|
-
# Get metadata with indifferent access (string/symbol keys)
|
|
183
|
-
# Returns empty hash if nil (for MySQL compatibility where JSON columns can't have defaults)
|
|
184
|
-
def metadata
|
|
185
|
-
@indifferent_metadata ||= ActiveSupport::HashWithIndifferentAccess.new(super || {})
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Set metadata, ensuring consistent storage format
|
|
189
|
-
def metadata=(hash)
|
|
190
|
-
@indifferent_metadata = nil # Clear cache
|
|
191
|
-
super(hash.is_a?(Hash) ? hash.to_h : {})
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Clear metadata cache on reload to ensure fresh data from database
|
|
195
|
-
def reload(*)
|
|
196
|
-
@indifferent_metadata = nil
|
|
197
|
-
super
|
|
198
|
-
end
|
|
199
|
-
|
|
200
111
|
private
|
|
201
112
|
|
|
202
|
-
# Sync in-place modifications to the cached metadata back to the attribute
|
|
203
|
-
# This ensures changes like `metadata["key"] = "value"` are persisted on save
|
|
204
|
-
# Also ensures metadata is never null for MySQL compatibility (JSON columns can't have defaults)
|
|
205
|
-
def sync_metadata_cache
|
|
206
|
-
if @indifferent_metadata
|
|
207
|
-
write_attribute(:metadata, @indifferent_metadata.to_h)
|
|
208
|
-
elsif read_attribute(:metadata).nil?
|
|
209
|
-
write_attribute(:metadata, {})
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
113
|
# Format operation charge descriptions (e.g., "Process Video (-10 credits)")
|
|
214
114
|
def operation_description
|
|
215
115
|
operation = metadata["operation"]&.to_s&.titleize
|
|
@@ -220,12 +120,5 @@ module UsageCredits
|
|
|
220
120
|
|
|
221
121
|
"#{operation} (-#{cost} credits)"
|
|
222
122
|
end
|
|
223
|
-
|
|
224
|
-
def remaining_amount_cannot_be_negative
|
|
225
|
-
if credit? && remaining_amount < 0
|
|
226
|
-
errors.add(:base, "Allocated amount exceeds transaction amount")
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
123
|
end
|
|
231
124
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsageCredits
|
|
4
|
+
# A transfer records an internal movement of credits between two wallets.
|
|
5
|
+
# The actual balance impact lives in the linked transactions on each side
|
|
6
|
+
# so the transaction history remains explicit.
|
|
7
|
+
#
|
|
8
|
+
# This class extends Wallets::Transfer with usage_credits table configuration.
|
|
9
|
+
|
|
10
|
+
class Transfer < Wallets::TransferBase
|
|
11
|
+
# =========================================
|
|
12
|
+
# Embeddability Configuration
|
|
13
|
+
# =========================================
|
|
14
|
+
|
|
15
|
+
self.embedded_table_name = "usage_credits_transfers"
|
|
16
|
+
self.config_provider = -> { UsageCredits.configuration }
|
|
17
|
+
self.transaction_class_name = "UsageCredits::Transaction"
|
|
18
|
+
|
|
19
|
+
# =========================================
|
|
20
|
+
# Re-declare Associations with Correct Classes
|
|
21
|
+
# =========================================
|
|
22
|
+
|
|
23
|
+
belongs_to :from_wallet,
|
|
24
|
+
class_name: "UsageCredits::Wallet",
|
|
25
|
+
inverse_of: :outgoing_transfers,
|
|
26
|
+
optional: false
|
|
27
|
+
belongs_to :to_wallet,
|
|
28
|
+
class_name: "UsageCredits::Wallet",
|
|
29
|
+
inverse_of: :incoming_transfers,
|
|
30
|
+
optional: false
|
|
31
|
+
has_many :transactions,
|
|
32
|
+
class_name: "UsageCredits::Transaction",
|
|
33
|
+
foreign_key: :transfer_id,
|
|
34
|
+
inverse_of: :transfer,
|
|
35
|
+
dependent: :nullify
|
|
36
|
+
end
|
|
37
|
+
end
|