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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +246 -13
  4. data/lib/generators/usage_credits/install_generator.rb +9 -1
  5. data/lib/generators/usage_credits/templates/create_usage_credits_tables.rb.erb +65 -21
  6. data/lib/generators/usage_credits/templates/upgrade_usage_credits_to_wallets_core.rb.erb +602 -0
  7. data/lib/generators/usage_credits/upgrade_generator.rb +47 -0
  8. data/lib/usage_credits/callbacks.rb +9 -54
  9. data/lib/usage_credits/configuration.rb +43 -13
  10. data/lib/usage_credits/core_ext/numeric.rb +12 -16
  11. data/lib/usage_credits/cost/base.rb +1 -6
  12. data/lib/usage_credits/cost/compound.rb +3 -2
  13. data/lib/usage_credits/cost/fixed.rb +1 -2
  14. data/lib/usage_credits/cost/variable.rb +23 -8
  15. data/lib/usage_credits/engine.rb +8 -12
  16. data/lib/usage_credits/helpers/credit_calculator.rb +18 -0
  17. data/lib/usage_credits/helpers/credits_helper.rb +9 -11
  18. data/lib/usage_credits/helpers/period_parser.rb +31 -9
  19. data/lib/usage_credits/helpers/processor_metadata.rb +46 -0
  20. data/lib/usage_credits/jobs/fulfillment_job.rb +1 -1
  21. data/lib/usage_credits/models/allocation.rb +15 -20
  22. data/lib/usage_credits/models/concerns/has_wallet.rb +50 -27
  23. data/lib/usage_credits/models/concerns/pay_charge_extension.rb +184 -139
  24. data/lib/usage_credits/models/concerns/pay_subscription_extension.rb +490 -174
  25. data/lib/usage_credits/models/credit_pack.rb +34 -13
  26. data/lib/usage_credits/models/credit_subscription_plan.rb +36 -15
  27. data/lib/usage_credits/models/fulfillment.rb +23 -48
  28. data/lib/usage_credits/models/operation.rb +36 -28
  29. data/lib/usage_credits/models/transaction.rb +55 -152
  30. data/lib/usage_credits/models/transfer.rb +37 -0
  31. data/lib/usage_credits/models/wallet.rb +219 -314
  32. data/lib/usage_credits/railtie.rb +4 -13
  33. data/lib/usage_credits/services/fulfillment_service.rb +146 -22
  34. data/lib/usage_credits/subscription_terms.rb +131 -0
  35. data/lib/usage_credits/version.rb +1 -1
  36. data/lib/usage_credits.rb +16 -3
  37. metadata +33 -20
  38. data/.rubocop.yml +0 -8
  39. data/.simplecov +0 -48
  40. data/AGENTS.md +0 -5
  41. data/Appraisals +0 -43
  42. data/CLAUDE.md +0 -5
  43. data/Rakefile +0 -32
  44. data/gemfiles/pay_10.0.gemfile +0 -41
  45. data/gemfiles/pay_11.0.gemfile +0 -41
  46. data/gemfiles/pay_8.3.gemfile +0 -41
  47. data/gemfiles/pay_9.0.gemfile +0 -41
  48. data/gemfiles/rails_7.2.gemfile +0 -41
  49. data/gemfiles/rails_8.1.gemfile +0 -41
@@ -6,15 +6,12 @@ module UsageCredits
6
6
  failed = 0
7
7
 
8
8
  Fulfillment.due_for_fulfillment.find_each do |fulfillment|
9
- begin
10
- new(fulfillment).process
11
- count += 1
12
- rescue StandardError => e
13
- failed += 1
14
- Rails.logger.error "Failed to process fulfillment #{fulfillment.id}: #{e.message}"
15
- Rails.logger.error e.backtrace.join("\n")
16
- next # Continue with next fulfillment
17
- end
9
+ count += 1 if new(fulfillment).process
10
+ rescue => e
11
+ failed += 1
12
+ Rails.logger.error "Failed to process fulfillment #{fulfillment.id}: #{e.message}"
13
+ Rails.logger.error e.backtrace.join("\n")
14
+ next # Continue with next fulfillment
18
15
  end
19
16
 
20
17
  Rails.logger.info "Processed #{count} fulfillments (#{failed} failed)"
@@ -27,26 +24,111 @@ module UsageCredits
27
24
  end
28
25
 
29
26
  def process
30
- ActiveRecord::Base.transaction do
27
+ credit_transaction = nil
28
+
29
+ reconcile_subscription_source!
30
+
31
+ @fulfillment.class.transaction do
32
+ lock_subscription_source!
31
33
  @fulfillment.lock! # row lock to avoid double awarding
32
34
 
33
35
  # re-check if it's still due, in case time changed or another process already updated it
34
- return unless @fulfillment.due_for_fulfillment?
36
+ next unless @fulfillment.due_for_fulfillment?
37
+ next unless subscription_source_eligible?
35
38
 
36
39
  credits = calculate_credits
37
- give_credits(credits)
40
+ unless credits.is_a?(Integer) && credits.positive?
41
+ raise UsageCredits::Error, "Fulfillment credits must be a positive whole number"
42
+ end
43
+
44
+ credit_transaction = give_credits(credits)
38
45
  update_fulfillment(credits)
39
46
  end
47
+
48
+ dispatch_subscription_callback(credit_transaction) if credit_transaction
49
+ credit_transaction
40
50
  rescue UsageCredits::Error => e
41
51
  Rails.logger.error "Usage credits error processing fulfillment #{@fulfillment.id}: #{e.message}"
42
52
  raise
43
- rescue StandardError => e
53
+ rescue => e
44
54
  Rails.logger.error "Unexpected error processing fulfillment #{@fulfillment.id}: #{e.message}"
45
55
  raise
46
56
  end
47
57
 
48
58
  private
49
59
 
60
+ def reconcile_subscription_source!
61
+ return unless @fulfillment.fulfillment_type == "subscription"
62
+ return unless @fulfillment.metadata["initial_award_state"] == "trial" ||
63
+ @fulfillment.metadata.key?("deferred_plan_change")
64
+
65
+ source = @fulfillment.source
66
+ return unless source.is_a?(Pay::Subscription)
67
+ return unless source.eligible_for_usage_credit_fulfillment?
68
+
69
+ source.sync_usage_credit_fulfillment!
70
+ @fulfillment.reload
71
+ end
72
+
73
+ def lock_subscription_source!
74
+ @pay_subscription_source = false
75
+ @locked_subscription_source = nil
76
+ return unless @fulfillment.fulfillment_type == "subscription"
77
+
78
+ # A persisted polymorphic type means this fulfillment was explicitly
79
+ # tied to Pay. Do not reinterpret a dangling reference as a legacy
80
+ # source-less/custom subscription and continue minting credits.
81
+ source_class = @fulfillment.source_type&.safe_constantize
82
+ @pay_subscription_source = source_class && source_class <= Pay::Subscription
83
+ source = @fulfillment.source
84
+ return unless @pay_subscription_source || source.is_a?(Pay::Subscription)
85
+
86
+ unless source
87
+ raise UsageCredits::Error,
88
+ "Pay subscription #{@fulfillment.source_id} for fulfillment #{@fulfillment.id} no longer exists"
89
+ end
90
+
91
+ # All subscription writers use subscription → fulfillment → wallet lock
92
+ # order. Taking the processor row first makes the status check and credit
93
+ # mint linearizable with cancellation/activation webhooks.
94
+ @pay_subscription_source = true
95
+ @locked_subscription_source = source.class.lock.find_by(id: source.id)
96
+ unless @locked_subscription_source
97
+ raise UsageCredits::Error,
98
+ "Pay subscription #{source.id} for fulfillment #{@fulfillment.id} no longer exists"
99
+ end
100
+ end
101
+
102
+ def subscription_source_eligible?
103
+ return true unless @fulfillment.fulfillment_type == "subscription"
104
+ return true unless @pay_subscription_source
105
+
106
+ # A stale/delayed job must never keep minting recurring credits while the
107
+ # processor subscription is trialing, canceled, paused, or incomplete.
108
+ # Pay's processor-specific predicate handles Stripe pauses whose raw
109
+ # status remains "active" until the subscription is resumed.
110
+ source = @locked_subscription_source
111
+ return false unless source&.eligible_for_usage_credit_fulfillment?
112
+
113
+ if @fulfillment.metadata.key?("deferred_plan_change")
114
+ raise UsageCredits::InvalidOperation,
115
+ "Deferred plan change for fulfillment #{@fulfillment.id} was not reconciled"
116
+ end
117
+
118
+ source_plan = source.processor_plan.to_s
119
+ active_plan = @fulfillment.metadata["plan"].to_s
120
+ resolved_transition = source_plan == active_plan ||
121
+ @fulfillment.metadata["pending_plan_change"].to_s == source_plan ||
122
+ @fulfillment.metadata["stopped_plan"].to_s == source_plan
123
+
124
+ unless resolved_transition
125
+ raise UsageCredits::InvalidOperation,
126
+ "Pay subscription #{source.id} plan transition has not been reconciled for fulfillment #{@fulfillment.id}"
127
+ end
128
+
129
+ true
130
+ end
131
+
50
132
  def validate_fulfillment!
51
133
  raise UsageCredits::Error, "No fulfillment provided" if @fulfillment.nil?
52
134
  raise UsageCredits::Error, "Invalid fulfillment type" unless ["subscription", "credit_pack", "manual"].include?(@fulfillment.fulfillment_type)
@@ -57,7 +139,8 @@ module UsageCredits
57
139
  when "subscription"
58
140
  raise UsageCredits::Error, "No plan specified in metadata" unless @fulfillment.metadata["plan"].present?
59
141
  when "credit_pack"
60
- raise UsageCredits::Error, "No pack specified in metadata" unless @fulfillment.metadata["pack"].present?
142
+ pack_name = @fulfillment.metadata["pack"] || @fulfillment.metadata["pack_name"]
143
+ raise UsageCredits::Error, "No pack specified in metadata" unless pack_name.present?
61
144
  else
62
145
  raise UsageCredits::Error, "No credits amount specified in metadata" unless @fulfillment.metadata["credits"].present?
63
146
  end
@@ -73,6 +156,26 @@ module UsageCredits
73
156
  )
74
157
  end
75
158
 
159
+ def dispatch_subscription_callback(transaction)
160
+ return unless @fulfillment.fulfillment_type == "subscription"
161
+
162
+ ActiveRecord.after_all_transactions_commit do
163
+ UsageCredits::Callbacks.dispatch(
164
+ :subscription_credits_awarded,
165
+ wallet: @fulfillment.wallet,
166
+ amount: transaction.amount,
167
+ transaction: transaction,
168
+ metadata: {
169
+ fulfillment: @fulfillment,
170
+ subscription_plan_name: @plan&.name,
171
+ pay_subscription: @fulfillment.source,
172
+ fulfillment_period: @fulfillment.fulfillment_period,
173
+ reason: "fulfillment_cycle"
174
+ }
175
+ )
176
+ end
177
+ end
178
+
76
179
  def update_fulfillment(credits)
77
180
  @fulfillment.update!(
78
181
  last_fulfilled_at: Time.current,
@@ -85,24 +188,37 @@ module UsageCredits
85
188
  case @fulfillment.fulfillment_type
86
189
  when "subscription"
87
190
  @plan = UsageCredits.find_subscription_plan_by_processor_id(@fulfillment.metadata["plan"])
88
- raise UsageCredits::InvalidOperation, "No subscription plan found for processor ID #{@fulfillment.metadata["plan"]}" unless @plan
89
- @plan.credits_per_period
191
+ snapshot_credits = @fulfillment.metadata["credits_per_period"]
192
+ if @fulfillment.metadata.key?("credits_per_period")
193
+ strict_positive_integer(snapshot_credits, "credits_per_period")
194
+ elsif @plan
195
+ @plan.credits_per_period
196
+ else
197
+ raise UsageCredits::InvalidOperation, "No subscription plan found for processor ID #{@fulfillment.metadata["plan"]} and no persisted credit snapshot"
198
+ end
90
199
  when "credit_pack"
91
- pack = UsageCredits.find_credit_pack(@fulfillment.metadata["pack"])
92
- raise UsageCredits::InvalidOperation, "No credit pack named #{@fulfillment.metadata["pack"]}" unless pack
200
+ pack_name = @fulfillment.metadata["pack"] || @fulfillment.metadata["pack_name"]
201
+ pack = UsageCredits.find_credit_pack(pack_name)
202
+ raise UsageCredits::InvalidOperation, "No credit pack named #{pack_name}" unless pack
93
203
  pack.total_credits
94
204
  else
95
- @fulfillment.metadata["credits"].to_i
205
+ strict_positive_integer(@fulfillment.metadata["credits"], "credits")
96
206
  end
97
207
  end
98
208
 
99
209
  def calculate_expiration
100
- return nil unless @fulfillment.fulfillment_type == "subscription" && @plan
101
- return nil if @plan.rollover_enabled
210
+ return nil unless @fulfillment.fulfillment_type == "subscription"
211
+
212
+ rollover = if @fulfillment.metadata.key?("rollover_enabled")
213
+ ActiveModel::Type::Boolean.new.cast(@fulfillment.metadata["rollover_enabled"])
214
+ else
215
+ @plan&.rollover_enabled
216
+ end
217
+ return nil if rollover
102
218
 
103
219
  # Cap the grace period to the fulfillment period to prevent balance accumulation
104
220
  # when fulfillment_period << grace_period (e.g., 15 seconds vs 5 minutes)
105
- fulfillment_period = @plan.parsed_fulfillment_period
221
+ fulfillment_period = UsageCredits::PeriodParser.parse_persisted_period(@fulfillment.fulfillment_period)
106
222
  effective_grace = [
107
223
  UsageCredits.configuration.fulfillment_grace_period,
108
224
  fulfillment_period
@@ -111,6 +227,14 @@ module UsageCredits
111
227
  @fulfillment.calculate_next_fulfillment + effective_grace
112
228
  end
113
229
 
230
+ def strict_positive_integer(value, name)
231
+ number = Wallets::WholeNumber.parse(value, name: name, allow_string: true)
232
+ raise UsageCredits::Error, "#{name} must be positive" unless number.positive?
233
+ number
234
+ rescue ArgumentError
235
+ raise UsageCredits::Error, "#{name} must be a positive whole number"
236
+ end
237
+
114
238
  def fulfillment_category
115
239
  case @fulfillment.fulfillment_type
116
240
  when "subscription" then "subscription_credits"
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UsageCredits
4
+ # Immutable commercial terms used to fulfill one processor subscription.
5
+ # Terms may come from checkout metadata, an existing fulfillment snapshot,
6
+ # or (for legacy records) the current Ruby configuration.
7
+ class SubscriptionTerms
8
+ TRUE_VALUES = [true, 1, "1", "true"].freeze
9
+ FALSE_VALUES = [false, 0, "0", "false"].freeze
10
+
11
+ attr_reader :name,
12
+ :processor_plan_id,
13
+ :credits_per_period,
14
+ :signup_bonus_credits,
15
+ :trial_credits,
16
+ :fulfillment_period_display,
17
+ :rollover_enabled,
18
+ :expire_credits_on_cancel,
19
+ :credit_expiration_period_seconds,
20
+ :configured_plan
21
+
22
+ def self.from_plan(plan, processor_plan_id:)
23
+ return if plan.nil?
24
+
25
+ new(
26
+ name: plan.name,
27
+ processor_plan_id: processor_plan_id,
28
+ credits_per_period: plan.credits_per_period,
29
+ signup_bonus_credits: plan.signup_bonus_credits,
30
+ trial_credits: plan.trial_credits,
31
+ fulfillment_period: plan.fulfillment_period_display,
32
+ rollover_enabled: plan.rollover_enabled,
33
+ expire_credits_on_cancel: plan.expire_credits_on_cancel,
34
+ credit_expiration_period: plan.credit_expiration_period&.to_i,
35
+ configured_plan: plan
36
+ )
37
+ end
38
+
39
+ def self.from_metadata(metadata, processor_plan_id:, configured_plan: nil)
40
+ data = (metadata || {}).with_indifferent_access
41
+ return unless data.key?(:credits_per_period) && data.key?(:fulfillment_period) && data.key?(:rollover_enabled)
42
+
43
+ new(
44
+ name: data[:plan_name].presence || data[:subscription_name].presence || configured_plan&.name || processor_plan_id,
45
+ processor_plan_id: processor_plan_id,
46
+ credits_per_period: data[:credits_per_period],
47
+ signup_bonus_credits: data.fetch(:signup_bonus_credits, 0),
48
+ trial_credits: data.fetch(:trial_credits, 0),
49
+ fulfillment_period: data[:fulfillment_period],
50
+ rollover_enabled: parse_boolean(data[:rollover_enabled], "rollover_enabled"),
51
+ expire_credits_on_cancel: boolean_from_metadata(
52
+ data,
53
+ :expire_credits_on_cancel,
54
+ fallback: configured_plan&.expire_credits_on_cancel || false
55
+ ),
56
+ credit_expiration_period: value_from_metadata(
57
+ data,
58
+ :credit_expiration_period,
59
+ fallback: configured_plan&.credit_expiration_period&.to_i
60
+ ),
61
+ configured_plan: configured_plan,
62
+ allow_string_numbers: true
63
+ )
64
+ end
65
+
66
+ def self.parse_boolean(value, name)
67
+ return true if TRUE_VALUES.include?(value)
68
+ return false if FALSE_VALUES.include?(value)
69
+
70
+ raise ArgumentError, "#{name} must be true or false"
71
+ end
72
+ private_class_method :parse_boolean
73
+
74
+ def self.boolean_from_metadata(data, key, fallback:)
75
+ value = value_from_metadata(data, key, fallback: fallback)
76
+ parse_boolean(value, key)
77
+ end
78
+ private_class_method :boolean_from_metadata
79
+
80
+ def self.value_from_metadata(data, key, fallback:)
81
+ value = data[key]
82
+ (value.nil? || value == "") ? fallback : value
83
+ end
84
+ private_class_method :value_from_metadata
85
+
86
+ def initialize(name:, processor_plan_id:, credits_per_period:, signup_bonus_credits:, trial_credits:,
87
+ fulfillment_period:, rollover_enabled:, expire_credits_on_cancel: false, credit_expiration_period: nil,
88
+ configured_plan: nil, allow_string_numbers: false)
89
+ @name = name.respond_to?(:to_sym) ? name.to_sym : name
90
+ @processor_plan_id = processor_plan_id.to_s
91
+ @credits_per_period = parse_positive(credits_per_period, "credits_per_period", allow_string_numbers)
92
+ @signup_bonus_credits = parse_non_negative(signup_bonus_credits, "signup_bonus_credits", allow_string_numbers)
93
+ @trial_credits = parse_non_negative(trial_credits, "trial_credits", allow_string_numbers)
94
+ @fulfillment_period_display = fulfillment_period.to_s
95
+ @parsed_fulfillment_period = UsageCredits::PeriodParser.parse_persisted_period(@fulfillment_period_display)
96
+ @rollover_enabled = rollover_enabled == true
97
+ @expire_credits_on_cancel = expire_credits_on_cancel == true
98
+ @credit_expiration_period_seconds = parse_expiration_period(credit_expiration_period, allow_string_numbers)
99
+ @configured_plan = configured_plan
100
+ freeze
101
+ end
102
+
103
+ attr_reader :parsed_fulfillment_period
104
+
105
+ def callback_plan
106
+ configured_plan || self
107
+ end
108
+
109
+ private
110
+
111
+ def parse_non_negative(value, name, allow_string)
112
+ number = Wallets::WholeNumber.parse(value, name: name, allow_string: allow_string)
113
+ raise ArgumentError, "#{name} cannot be negative" if number.negative?
114
+
115
+ number
116
+ end
117
+
118
+ def parse_positive(value, name, allow_string)
119
+ number = parse_non_negative(value, name, allow_string)
120
+ raise ArgumentError, "#{name} must be positive" unless number.positive?
121
+
122
+ number
123
+ end
124
+
125
+ def parse_expiration_period(value, allow_string)
126
+ return 0 if value.nil? || value == ""
127
+
128
+ parse_non_negative(value, "credit_expiration_period", allow_string)
129
+ end
130
+ end
131
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UsageCredits
4
- VERSION = "0.4.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/usage_credits.rb CHANGED
@@ -5,14 +5,25 @@
5
5
 
6
6
  require "rails"
7
7
  require "active_record"
8
+ require "active_job"
8
9
  require "pay"
10
+ require "wallets"
9
11
  require "active_support/all"
10
12
 
13
+ module UsageCredits
14
+ # usage_credits deliberately exposes one asset. Keep its persisted value in
15
+ # one runtime constant so association lookup and race-safe creation cannot
16
+ # drift apart.
17
+ DEFAULT_ASSET_CODE = "credits"
18
+ end
19
+
11
20
  # Load order matters! Dependencies are loaded in this specific order:
12
21
  #
13
22
  # 1. Core helpers
14
23
  require "usage_credits/helpers/credit_calculator" # Centralized credit rounding
15
24
  require "usage_credits/helpers/period_parser" # Parse fulfillment periods like `:monthly` to `1.month`
25
+ require "usage_credits/helpers/processor_metadata" # Payment metadata limits and serialization
26
+ require "usage_credits/subscription_terms" # Immutable processor subscription snapshots
16
27
  require "usage_credits/core_ext/numeric" # Numeric extension to write `10.credits` in our DSL
17
28
 
18
29
  # 2. Cost calculation
@@ -43,17 +54,19 @@ module UsageCredits
43
54
  end
44
55
 
45
56
  # 6. Models (order matters for dependencies)
57
+ # These extend Wallets::* classes, so wallets gem must be loaded first
46
58
  require "usage_credits/models/wallet"
47
59
  require "usage_credits/models/transaction"
48
60
  require "usage_credits/models/allocation"
61
+ require "usage_credits/models/transfer"
49
62
  require "usage_credits/models/operation"
50
63
  require "usage_credits/models/fulfillment"
51
64
  require "usage_credits/models/credit_pack"
52
65
  require "usage_credits/models/credit_subscription_plan"
53
66
 
54
67
  # 7. Jobs
55
- require "usage_credits/services/fulfillment_service.rb"
56
- require "usage_credits/jobs/fulfillment_job.rb"
68
+ require "usage_credits/services/fulfillment_service"
69
+ require "usage_credits/jobs/fulfillment_job"
57
70
 
58
71
  # Main module that serves as the primary interface to the gem.
59
72
  # Most methods here delegate to Configuration, which is the single source of truth for all config in the initializer
@@ -62,6 +75,7 @@ module UsageCredits
62
75
  class Error < StandardError; end
63
76
  class InsufficientCredits < Error; end
64
77
  class InvalidOperation < Error; end
78
+ class InvalidTransfer < Error; end
65
79
 
66
80
  class << self
67
81
  attr_writer :configuration
@@ -157,7 +171,6 @@ module UsageCredits
157
171
  notify_low_balance(params[:wallet]&.owner)
158
172
  end
159
173
  end
160
-
161
174
  end
162
175
  end
163
176
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: usage_credits
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-16 00:00:00.000000000 Z
10
+ date: 2026-10-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pay
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '8.3'
18
+ version: 11.6.2
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '12.0'
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '8.3'
28
+ version: 11.6.2
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '12.0'
@@ -35,14 +35,34 @@ dependencies:
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '6.1'
38
+ version: 7.2.3.1
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9.0'
39
42
  type: :runtime
40
43
  prerelease: false
41
44
  version_requirements: !ruby/object:Gem::Requirement
42
45
  requirements:
43
46
  - - ">="
44
47
  - !ruby/object:Gem::Version
45
- version: '6.1'
48
+ version: 7.2.3.1
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: wallets
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: 0.4.0
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: 0.4.0
46
66
  description: Add a usage-based credit system to your Rails app, easily. Let users
47
67
  buy and spend credits on usage-based actions. Refill Stripe subscriptions with credits,
48
68
  sell one-time booster credit packs, implement PAYG (pay-as-you-go) billing, award
@@ -55,24 +75,14 @@ executables: []
55
75
  extensions: []
56
76
  extra_rdoc_files: []
57
77
  files:
58
- - ".rubocop.yml"
59
- - ".simplecov"
60
- - AGENTS.md
61
- - Appraisals
62
78
  - CHANGELOG.md
63
- - CLAUDE.md
64
79
  - LICENSE.txt
65
80
  - README.md
66
- - Rakefile
67
- - gemfiles/pay_10.0.gemfile
68
- - gemfiles/pay_11.0.gemfile
69
- - gemfiles/pay_8.3.gemfile
70
- - gemfiles/pay_9.0.gemfile
71
- - gemfiles/rails_7.2.gemfile
72
- - gemfiles/rails_8.1.gemfile
73
81
  - lib/generators/usage_credits/install_generator.rb
74
82
  - lib/generators/usage_credits/templates/create_usage_credits_tables.rb.erb
75
83
  - lib/generators/usage_credits/templates/initializer.rb
84
+ - lib/generators/usage_credits/templates/upgrade_usage_credits_to_wallets_core.rb.erb
85
+ - lib/generators/usage_credits/upgrade_generator.rb
76
86
  - lib/usage_credits.rb
77
87
  - lib/usage_credits/callback_context.rb
78
88
  - lib/usage_credits/callbacks.rb
@@ -86,6 +96,7 @@ files:
86
96
  - lib/usage_credits/helpers/credit_calculator.rb
87
97
  - lib/usage_credits/helpers/credits_helper.rb
88
98
  - lib/usage_credits/helpers/period_parser.rb
99
+ - lib/usage_credits/helpers/processor_metadata.rb
89
100
  - lib/usage_credits/jobs/fulfillment_job.rb
90
101
  - lib/usage_credits/models/allocation.rb
91
102
  - lib/usage_credits/models/concerns/has_wallet.rb
@@ -96,9 +107,11 @@ files:
96
107
  - lib/usage_credits/models/fulfillment.rb
97
108
  - lib/usage_credits/models/operation.rb
98
109
  - lib/usage_credits/models/transaction.rb
110
+ - lib/usage_credits/models/transfer.rb
99
111
  - lib/usage_credits/models/wallet.rb
100
112
  - lib/usage_credits/railtie.rb
101
113
  - lib/usage_credits/services/fulfillment_service.rb
114
+ - lib/usage_credits/subscription_terms.rb
102
115
  - lib/usage_credits/version.rb
103
116
  - sig/usagecredits.rbs
104
117
  homepage: https://github.com/rameerez/usage_credits
@@ -107,7 +120,7 @@ licenses:
107
120
  metadata:
108
121
  allowed_push_host: https://rubygems.org
109
122
  homepage_uri: https://github.com/rameerez/usage_credits
110
- source_code_uri: https://github.com/rameerez/usage_credits
123
+ source_code_uri: https://github.com/rameerez/usage_credits/tree/main
111
124
  changelog_uri: https://github.com/rameerez/usage_credits/blob/main/CHANGELOG.md
112
125
  rubygems_mfa_required: 'true'
113
126
  rdoc_options: []
@@ -117,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
130
  requirements:
118
131
  - - ">="
119
132
  - !ruby/object:Gem::Version
120
- version: 3.1.0
133
+ version: 3.2.0
121
134
  required_rubygems_version: !ruby/object:Gem::Requirement
122
135
  requirements:
123
136
  - - ">="
data/.rubocop.yml DELETED
@@ -1,8 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
3
-
4
- Style/StringLiterals:
5
- EnforcedStyle: double_quotes
6
-
7
- Style/StringLiteralsInInterpolation:
8
- EnforcedStyle: double_quotes
data/.simplecov DELETED
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- SimpleCov.start 'rails' do
4
- # Coverage directory
5
- coverage_dir 'coverage'
6
-
7
- # Enable branch coverage (must be before minimum_coverage)
8
- enable_coverage :branch
9
-
10
- # Set minimum coverage threshold to prevent coverage regression
11
- # Current coverage: Line 84.38%, Branch 71.9%
12
- # Note: Some paths (PostgreSQL JSON operators, error fallbacks) may not be exercised in SQLite tests
13
- minimum_coverage line: 75, branch: 60
14
-
15
- # Add custom groups for better organization
16
- add_group 'Models', 'lib/usage_credits/models'
17
- add_group 'Services', 'lib/usage_credits/services'
18
- add_group 'Helpers', 'lib/usage_credits/helpers'
19
- add_group 'Jobs', 'lib/usage_credits/jobs'
20
- add_group 'Concerns', 'lib/usage_credits/models/concerns'
21
- add_group 'DSL', ['lib/usage_credits/operation.rb', 'lib/usage_credits/credit_pack.rb', 'lib/usage_credits/credit_subscription_plan.rb']
22
-
23
- # Filter out files we don't want to track
24
- add_filter '/test/'
25
- add_filter '/spec/'
26
- add_filter '/config/'
27
- add_filter '/db/'
28
- add_filter '/vendor/'
29
- add_filter '/bin/'
30
-
31
- # Track all Ruby files in lib
32
- track_files 'lib/**/*.rb'
33
-
34
- # Disambiguate parallel test runs
35
- command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
36
-
37
- # Use different formatters for CI vs local
38
- if ENV['CI']
39
- # CI: Use simple formatter for console output
40
- formatter SimpleCov::Formatter::SimpleFormatter
41
- else
42
- # Local: Use HTML formatter for detailed report
43
- formatter SimpleCov::Formatter::HTMLFormatter
44
- end
45
-
46
- # Merge results from parallel runs
47
- merge_timeout 3600
48
- end
data/AGENTS.md DELETED
@@ -1,5 +0,0 @@
1
- # AGENTS.md
2
-
3
- This file provides guidance to AI Agents (like OpenAI's Codex, Claude Code, etc) when working with code in this repository.
4
-
5
- Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
data/Appraisals DELETED
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Test minimum supported Rails version (with latest Pay)
4
- appraise "rails-7.2" do
5
- gem "rails", "~> 7.2.0"
6
- gem "pay", "~> 11.0"
7
- gem "stripe", "~> 18.0"
8
- end
9
-
10
- # Test latest Rails version (with latest Pay) - this is the default/main Gemfile anyway
11
- appraise "rails-8.1" do
12
- gem "rails", "~> 8.1.0"
13
- gem "pay", "~> 11.0"
14
- gem "stripe", "~> 18.0"
15
- end
16
-
17
- # Test minimum supported Pay version (with latest Rails)
18
- appraise "pay-8.3" do
19
- gem "pay", "~> 8.3.0"
20
- gem "stripe", "~> 13.0"
21
- gem "rails", "~> 8.1.0"
22
- end
23
-
24
- # Test Pay 9.0 (popular stable version with latest Rails)
25
- appraise "pay-9.0" do
26
- gem "pay", "~> 9.0.0"
27
- gem "stripe", "~> 13.0"
28
- gem "rails", "~> 8.1.0"
29
- end
30
-
31
- # Test Pay 10.0 (with latest Rails)
32
- appraise "pay-10.0" do
33
- gem "pay", "~> 10.0.0"
34
- gem "stripe", "~> 15.0"
35
- gem "rails", "~> 8.1.0"
36
- end
37
-
38
- # Test latest Pay version (with latest Rails)
39
- appraise "pay-11.0" do
40
- gem "pay", "~> 11.0"
41
- gem "stripe", "~> 18.0"
42
- gem "rails", "~> 8.1.0"
43
- end