usage_credits 0.4.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 +72 -0
- data/README.md +246 -13
- data/lib/generators/usage_credits/install_generator.rb +9 -1
- data/lib/generators/usage_credits/templates/create_usage_credits_tables.rb.erb +65 -21
- 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 +43 -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 +55 -152
- 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 -48
- 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,136 +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
|
-
#
|
|
19
|
-
|
|
23
|
+
# Override base categories with usage_credits-specific ones
|
|
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
|
|
|
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
|
+
|
|
57
|
+
def self.categories
|
|
58
|
+
(DEFAULT_CATEGORIES + resolved_config.additional_categories).uniq
|
|
59
|
+
end
|
|
60
|
+
|
|
43
61
|
# =========================================
|
|
44
|
-
# Associations
|
|
62
|
+
# Additional Associations
|
|
45
63
|
# =========================================
|
|
46
64
|
|
|
47
|
-
belongs_to :wallet
|
|
48
|
-
|
|
49
|
-
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
|
|
50
68
|
|
|
69
|
+
# Re-declare allocation associations with correct classes
|
|
51
70
|
has_many :outgoing_allocations,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
class_name: "UsageCredits::Allocation",
|
|
72
|
+
foreign_key: :transaction_id,
|
|
73
|
+
dependent: :destroy
|
|
55
74
|
|
|
56
75
|
has_many :incoming_allocations,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
validates :amount, presence: true, numericality: { only_integer: true }
|
|
62
|
-
validates :category, presence: true, inclusion: { in: CATEGORIES }
|
|
63
|
-
|
|
64
|
-
validate :remaining_amount_cannot_be_negative
|
|
76
|
+
class_name: "UsageCredits::Allocation",
|
|
77
|
+
foreign_key: :source_transaction_id,
|
|
78
|
+
dependent: :destroy
|
|
65
79
|
|
|
66
80
|
# =========================================
|
|
67
|
-
# Scopes
|
|
81
|
+
# Backwards Compatibility Scopes
|
|
68
82
|
# =========================================
|
|
69
83
|
|
|
70
|
-
scope :credits_added, -> { where(
|
|
71
|
-
scope :credits_deducted, -> { where(
|
|
72
|
-
scope :by_category, ->(category) { where(category: category) }
|
|
73
|
-
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)) }
|
|
74
86
|
scope :operation_charges, -> { where(category: :operation_charge) }
|
|
75
87
|
|
|
76
|
-
# A transaction is not expired if:
|
|
77
|
-
# 1. It has no expiration date, OR
|
|
78
|
-
# 2. Its expiration date is in the future
|
|
79
|
-
scope :not_expired, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
80
|
-
scope :expired, -> { where("expires_at < ?", Time.current) }
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# =========================================
|
|
84
|
-
# Helpers
|
|
85
|
-
# =========================================
|
|
86
|
-
|
|
87
|
-
# Get the owner of the wallet these credits belong to
|
|
88
|
-
def owner
|
|
89
|
-
wallet.owner
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Have these credits expired?
|
|
93
|
-
def expired?
|
|
94
|
-
expires_at.present? && expires_at < Time.current
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Is this transaction a positive credit or a negative (spend)?
|
|
98
|
-
def credit?
|
|
99
|
-
amount > 0
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def debit?
|
|
103
|
-
amount < 0
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# How many credits from this transaction have already been allocated (spent)?
|
|
107
|
-
# Only applies if this transaction is positive.
|
|
108
|
-
def allocated_amount
|
|
109
|
-
incoming_allocations.sum(:amount)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# How many credits remain unused in this positive transaction?
|
|
113
|
-
# If negative, this will effectively be 0.
|
|
114
|
-
def remaining_amount
|
|
115
|
-
return 0 unless credit?
|
|
116
|
-
amount - allocated_amount
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# =========================================
|
|
120
|
-
# Balance After Transaction
|
|
121
|
-
# =========================================
|
|
122
|
-
|
|
123
|
-
# Get the balance after this transaction was applied
|
|
124
|
-
# Returns nil for transactions created before this feature was added
|
|
125
|
-
def balance_after
|
|
126
|
-
metadata[:balance_after]
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Get the balance before this transaction was applied
|
|
130
|
-
# Returns the stored value if available, otherwise nil
|
|
131
|
-
# Note: For transactions created before this feature, returns nil
|
|
132
|
-
def balance_before
|
|
133
|
-
metadata[:balance_before]
|
|
134
|
-
end
|
|
135
|
-
|
|
136
88
|
# =========================================
|
|
137
89
|
# Display Formatting
|
|
138
90
|
# =========================================
|
|
@@ -144,7 +96,6 @@ module UsageCredits
|
|
|
144
96
|
end
|
|
145
97
|
|
|
146
98
|
# Format the balance after for display (e.g., "500 credits")
|
|
147
|
-
# Returns nil if balance_after is not stored
|
|
148
99
|
def formatted_balance_after
|
|
149
100
|
return nil unless balance_after
|
|
150
101
|
UsageCredits.configuration.credit_formatter.call(balance_after)
|
|
@@ -152,54 +103,13 @@ module UsageCredits
|
|
|
152
103
|
|
|
153
104
|
# Get a human-readable description of what this transaction represents
|
|
154
105
|
def description
|
|
155
|
-
# Custom description takes precedence
|
|
156
106
|
return self[:description] if self[:description].present?
|
|
157
|
-
|
|
158
|
-
# Operation charges have dynamic descriptions
|
|
159
107
|
return operation_description if category == "operation_charge"
|
|
160
|
-
|
|
161
|
-
# Use predefined description or fallback to titleized category
|
|
162
108
|
category.titleize
|
|
163
109
|
end
|
|
164
110
|
|
|
165
|
-
# =========================================
|
|
166
|
-
# Metadata Handling
|
|
167
|
-
# =========================================
|
|
168
|
-
|
|
169
|
-
# Sync in-place modifications to metadata before saving
|
|
170
|
-
before_save :sync_metadata_cache
|
|
171
|
-
|
|
172
|
-
# Get metadata with indifferent access (string/symbol keys)
|
|
173
|
-
# Returns empty hash if nil (for MySQL compatibility where JSON columns can't have defaults)
|
|
174
|
-
def metadata
|
|
175
|
-
@indifferent_metadata ||= ActiveSupport::HashWithIndifferentAccess.new(super || {})
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# Set metadata, ensuring consistent storage format
|
|
179
|
-
def metadata=(hash)
|
|
180
|
-
@indifferent_metadata = nil # Clear cache
|
|
181
|
-
super(hash.is_a?(Hash) ? hash.to_h : {})
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Clear metadata cache on reload to ensure fresh data from database
|
|
185
|
-
def reload(*)
|
|
186
|
-
@indifferent_metadata = nil
|
|
187
|
-
super
|
|
188
|
-
end
|
|
189
|
-
|
|
190
111
|
private
|
|
191
112
|
|
|
192
|
-
# Sync in-place modifications to the cached metadata back to the attribute
|
|
193
|
-
# This ensures changes like `metadata["key"] = "value"` are persisted on save
|
|
194
|
-
# Also ensures metadata is never null for MySQL compatibility (JSON columns can't have defaults)
|
|
195
|
-
def sync_metadata_cache
|
|
196
|
-
if @indifferent_metadata
|
|
197
|
-
write_attribute(:metadata, @indifferent_metadata.to_h)
|
|
198
|
-
elsif read_attribute(:metadata).nil?
|
|
199
|
-
write_attribute(:metadata, {})
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
113
|
# Format operation charge descriptions (e.g., "Process Video (-10 credits)")
|
|
204
114
|
def operation_description
|
|
205
115
|
operation = metadata["operation"]&.to_s&.titleize
|
|
@@ -210,12 +120,5 @@ module UsageCredits
|
|
|
210
120
|
|
|
211
121
|
"#{operation} (-#{cost} credits)"
|
|
212
122
|
end
|
|
213
|
-
|
|
214
|
-
def remaining_amount_cannot_be_negative
|
|
215
|
-
if credit? && remaining_amount < 0
|
|
216
|
-
errors.add(:base, "Allocated amount exceeds transaction amount")
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
123
|
end
|
|
221
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
|