usage_credits 0.2.1 → 0.3.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 +5 -0
- data/README.md +69 -13
- data/lib/generators/usage_credits/templates/initializer.rb +49 -9
- data/lib/usage_credits/callback_context.rb +28 -0
- data/lib/usage_credits/callbacks.rb +67 -0
- data/lib/usage_credits/configuration.rb +67 -1
- data/lib/usage_credits/models/concerns/pay_charge_extension.rb +40 -3
- data/lib/usage_credits/models/concerns/pay_subscription_extension.rb +89 -48
- data/lib/usage_credits/models/wallet.rb +55 -21
- data/lib/usage_credits/version.rb +1 -1
- data/lib/usage_credits.rb +19 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e1f36f964eea89e835d789f34b6b2ce8994940896d21652af2400282c0e24a0
|
|
4
|
+
data.tar.gz: ad7c6be299aee7f89929841bfe18a6f44e2dd91361afe30ff238980bdc75544b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4047f5a333b5e1359468468927100ea71a5c5b7117d8eec9e73a81ad8b40c243d796dd3d0e37604c9fd46b9b44239612d5bc1b30176e4322364312cc1c900017
|
|
7
|
+
data.tar.gz: b21b55c0b524b18fcf47339b926a425f53fcca5a3148f221bc8e86e4976f1281425041edd1d9cc903b68ab6cf82dd38a1a97df84373d1658d021cc43b3308de0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## [0.3.0] - 2026-01-15
|
|
2
|
+
|
|
3
|
+
- Add lifecycle callbacks by @rameerez in https://github.com/rameerez/usage_credits/pull/25
|
|
4
|
+
- Fix credit pack fulfillment not working with Pay 10+ (Stripe data in `object` vs `data` in `Pay::Charge`) by @rameerez in https://github.com/rameerez/usage_credits/pull/26
|
|
5
|
+
|
|
1
6
|
## [0.2.1] - 2026-01-15
|
|
2
7
|
|
|
3
8
|
- Add custom `create_checkout_session` options (like `success_url`) to credit pack purchases by @yshmarov in https://github.com/rameerez/usage_credits/pull/5
|
data/README.md
CHANGED
|
@@ -265,27 +265,83 @@ process_image(params) # If this fails, credits are already spent!
|
|
|
265
265
|
> If validation fails (e.g., file too large), both methods will raise `InvalidOperation`.
|
|
266
266
|
> Perform your operation inside the `spend_credits_on` block OR make the credit spend conditional to the actual operation, so users are not charged if the operation fails.
|
|
267
267
|
|
|
268
|
-
## Low balance alerts
|
|
268
|
+
## Low balance alerts & other lifecycle callbacks
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
`usage_credits` makes it easy to add callbacks for important events.
|
|
271
|
+
|
|
272
|
+
You can hook into credit events, for example, to upsell credit packs to users when they reach a low balance:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
config.on_low_balance_reached do |ctx|
|
|
276
|
+
# Sell your users credits here
|
|
277
|
+
# Example: LowCreditsMailer.buy_more(ctx.owner, remaining: ctx.new_balance).deliver_later
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
> [!TIP]
|
|
282
|
+
> For `on_low_balance_reached`, define what a low balance is by setting the `low_balance_threshold` like: `config.low_balance_threshold = 100.credits`
|
|
283
|
+
|
|
284
|
+
You configure callbacks in your `usage_credits.rb` initializer file.
|
|
285
|
+
|
|
286
|
+
Here are all the available callbacks you can hook into:
|
|
287
|
+
|
|
288
|
+
- `on_credits_added`: After credits are added to a wallet
|
|
289
|
+
- `on_credits_deducted`: After credits are deducted from a wallet
|
|
290
|
+
- `on_low_balance_reached`: When balance drops below threshold (fires once per crossing)
|
|
291
|
+
- `on_balance_depleted`: When balance reaches exactly zero
|
|
292
|
+
- `on_insufficient_credits`: When an operation fails due to insufficient credits
|
|
293
|
+
- `on_credit_pack_purchased`: After a credit pack purchase is fulfilled
|
|
294
|
+
- `on_subscription_credits_awarded`: After subscription credits are awarded
|
|
295
|
+
|
|
296
|
+
They're useful for analytics, audit logging, notifications, or custom business logic.
|
|
297
|
+
|
|
298
|
+
Callbacks are isolated, so errors in callbacks won't break credit operations.
|
|
299
|
+
|
|
300
|
+
> [!IMPORTANT]
|
|
301
|
+
> Callbacks get executed every single time the action happens (duh) so you don't want to do heavy operations there, or else your app could become extremely slow. Keep callbacks fast: one good option is using the callback to enqueue background jobs (`deliver_later`, `perform_later`) to avoid blocking credit operations.
|
|
302
|
+
|
|
303
|
+
### The `ctx` object in callbacks
|
|
304
|
+
|
|
305
|
+
All callbacks receive a context object (`ctx`). The `ctx` objects contains useful fields like `ctx.owner`, `ctx.amount`, etc. Some examples:
|
|
271
306
|
|
|
272
307
|
```ruby
|
|
273
308
|
UsageCredits.configure do |config|
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
309
|
+
# Prompt users to buy more credits when they run out
|
|
310
|
+
config.on_balance_depleted do |ctx|
|
|
311
|
+
# Send a mail here like "You've ran out of credits! Purchase more here!"
|
|
312
|
+
OutOfCreditsMailer.buy_more(ctx.owner).deliver_later
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Log failed operations (useful for debugging)
|
|
316
|
+
config.on_insufficient_credits do |ctx|
|
|
317
|
+
Rails.logger.info "[Credits] User #{ctx.owner.id} needs #{ctx.amount}, has #{ctx.metadata[:available]}"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Track purchases in your analytics (Mixpanel, Amplitude, Segment, etc.)
|
|
321
|
+
config.on_credit_pack_purchased do |ctx|
|
|
322
|
+
# Replace with your analytics service
|
|
323
|
+
# Log something like: "User #{ctx.owner.id} purchased #{ctx.amount} credits"
|
|
285
324
|
end
|
|
286
325
|
end
|
|
287
326
|
```
|
|
288
327
|
|
|
328
|
+
Available `ctx` fields vary by event:
|
|
329
|
+
|
|
330
|
+
- `credits_added`: `ctx.owner`, `ctx.wallet`, `ctx.amount`, `ctx.previous_balance`, `ctx.new_balance`, `ctx.transaction`, `ctx.category`, `ctx.metadata`
|
|
331
|
+
- `credits_deducted`: `ctx.owner`, `ctx.wallet`, `ctx.amount`, `ctx.previous_balance`, `ctx.new_balance`, `ctx.transaction`, `ctx.category`, `ctx.metadata`
|
|
332
|
+
- `low_balance_reached`: `ctx.owner`, `ctx.wallet`, `ctx.previous_balance`, `ctx.new_balance`, `ctx.threshold`
|
|
333
|
+
- `balance_depleted`: `ctx.owner`, `ctx.wallet`, `ctx.previous_balance`, `ctx.new_balance`
|
|
334
|
+
- `insufficient_credits`: `ctx.owner`, `ctx.wallet`, `ctx.amount`, `ctx.operation_name`, `ctx.metadata`
|
|
335
|
+
- `credit_pack_purchased`: `ctx.owner`, `ctx.wallet`, `ctx.amount`, `ctx.transaction`, `ctx.metadata`
|
|
336
|
+
- `subscription_credits_awarded`: `ctx.owner`, `ctx.wallet`, `ctx.amount`, `ctx.transaction`, `ctx.metadata`
|
|
337
|
+
|
|
338
|
+
All contexts support `ctx.to_h` to convert to a hash (excludes nil values).
|
|
339
|
+
|
|
340
|
+
**Metadata contents:**
|
|
341
|
+
- `insufficient_credits`: `{ available:, required:, params: }`
|
|
342
|
+
- `credit_pack_purchased`: `{ credit_pack_name:, credit_pack:, pay_charge:, price_cents: }`
|
|
343
|
+
- `subscription_credits_awarded`: `{ subscription_plan_name:, subscription:, pay_subscription:, fulfillment_period: }`
|
|
344
|
+
|
|
289
345
|
## Award bonus credits
|
|
290
346
|
|
|
291
347
|
You might want to award bonus credits to your users for arbitrary actions at any point, like referring a friend, completing signup, or any other reason.
|
|
@@ -82,18 +82,58 @@ UsageCredits.configure do |config|
|
|
|
82
82
|
# end
|
|
83
83
|
#
|
|
84
84
|
#
|
|
85
|
+
# === Lifecycle Callbacks ===
|
|
86
|
+
#
|
|
87
|
+
# Hook into credit events for analytics, notifications, and custom logic.
|
|
88
|
+
# All callbacks receive a context object with event-specific data.
|
|
89
|
+
#
|
|
90
|
+
# Available callbacks:
|
|
91
|
+
# on_credits_added - After credits are added to a wallet
|
|
92
|
+
# on_credits_deducted - After credits are deducted from a wallet
|
|
93
|
+
# on_low_balance_reached - When balance drops below threshold (fires once per crossing)
|
|
94
|
+
# on_balance_depleted - When balance reaches exactly zero
|
|
95
|
+
# on_insufficient_credits - When an operation fails due to insufficient credits
|
|
96
|
+
# on_credit_pack_purchased - After a credit pack purchase is fulfilled
|
|
97
|
+
# on_subscription_credits_awarded - After subscription credits are awarded
|
|
98
|
+
#
|
|
99
|
+
# Context object properties (available depending on event):
|
|
100
|
+
# ctx.event # Symbol - the event name
|
|
101
|
+
# ctx.owner # The wallet owner (User, Team, etc.)
|
|
102
|
+
# ctx.wallet # The UsageCredits::Wallet instance
|
|
103
|
+
# ctx.amount # Credits involved
|
|
104
|
+
# ctx.previous_balance # Balance before the operation
|
|
105
|
+
# ctx.new_balance # Balance after the operation
|
|
106
|
+
# ctx.transaction # The UsageCredits::Transaction record
|
|
107
|
+
# ctx.category # Transaction category (:manual_adjustment, :operation_charge, etc.)
|
|
108
|
+
# ctx.threshold # Low balance threshold (for low_balance_reached)
|
|
109
|
+
# ctx.operation_name # Operation name (for insufficient_credits)
|
|
110
|
+
# ctx.metadata # Additional context-specific data
|
|
111
|
+
# ctx.to_h # Convert to hash (excludes nil values)
|
|
112
|
+
#
|
|
113
|
+
# IMPORTANT: Keep callbacks fast! Use background jobs (deliver_later, perform_later) to avoid blocking credit operations.
|
|
114
|
+
#
|
|
115
|
+
# Example: Prompt user to buy more credits when running low
|
|
116
|
+
#
|
|
117
|
+
# config.low_balance_threshold = 100.credits # Set to nil to disable (default: 100)
|
|
118
|
+
#
|
|
119
|
+
# config.on_low_balance_reached do |ctx|
|
|
120
|
+
# LowCreditsMailer.buy_more(ctx.owner, remaining: ctx.new_balance).deliver_later
|
|
121
|
+
# end
|
|
85
122
|
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
123
|
+
# Example: Prompt user to buy credits when they run out:
|
|
124
|
+
# config.on_balance_depleted do |ctx|
|
|
125
|
+
# OutOfCreditsMailer.buy_more(ctx.owner).deliver_later
|
|
126
|
+
# end
|
|
91
127
|
#
|
|
92
|
-
#
|
|
128
|
+
# Example: Log when users hit credit limits (useful for debugging)
|
|
129
|
+
# config.on_insufficient_credits do |ctx|
|
|
130
|
+
# Rails.logger.info "[Credits] User #{ctx.owner.id} needs #{ctx.amount}, has #{ctx.metadata[:available]}"
|
|
131
|
+
# end
|
|
93
132
|
#
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
# Example: Track credit purchases (replace with your analytics service)
|
|
134
|
+
# config.on_credit_pack_purchased do |ctx|
|
|
135
|
+
# # e.g., Mixpanel, Amplitude, Segment, PostHog, etc.
|
|
136
|
+
# YourAnalyticsService.track(ctx.owner.id, "credits_purchased", amount: ctx.amount)
|
|
97
137
|
# end
|
|
98
138
|
#
|
|
99
139
|
#
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsageCredits
|
|
4
|
+
# Immutable context object passed to all callbacks
|
|
5
|
+
# Provides consistent, typed access to event data
|
|
6
|
+
CallbackContext = Struct.new(
|
|
7
|
+
:event, # Symbol - the event type
|
|
8
|
+
:wallet, # UsageCredits::Wallet instance
|
|
9
|
+
:amount, # Integer - credits involved (if applicable)
|
|
10
|
+
:previous_balance, # Integer - balance before operation
|
|
11
|
+
:new_balance, # Integer - balance after operation
|
|
12
|
+
:threshold, # Integer - low balance threshold (for low_balance events)
|
|
13
|
+
:category, # Symbol - transaction category
|
|
14
|
+
:operation_name, # Symbol - name of the operation
|
|
15
|
+
:transaction, # UsageCredits::Transaction - the transaction created
|
|
16
|
+
:metadata, # Hash - additional contextual data
|
|
17
|
+
keyword_init: true
|
|
18
|
+
) do
|
|
19
|
+
def to_h
|
|
20
|
+
super.compact
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Convenience: get owner from wallet
|
|
24
|
+
def owner
|
|
25
|
+
wallet&.owner
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsageCredits
|
|
4
|
+
# Centralized callback dispatch module
|
|
5
|
+
# Handles executing callbacks with error isolation
|
|
6
|
+
module Callbacks
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Dispatch a callback event with error isolation
|
|
10
|
+
# Callbacks should never break the main operation
|
|
11
|
+
#
|
|
12
|
+
# @param event [Symbol] The event type (e.g., :credits_added, :low_balance_reached)
|
|
13
|
+
# @param context_data [Hash] Data to pass to the callback via CallbackContext
|
|
14
|
+
def dispatch(event, **context_data)
|
|
15
|
+
config = UsageCredits.configuration
|
|
16
|
+
callback = config.public_send(:"on_#{event}_callback")
|
|
17
|
+
|
|
18
|
+
return unless callback.is_a?(Proc)
|
|
19
|
+
|
|
20
|
+
context = CallbackContext.new(event: event, **context_data)
|
|
21
|
+
|
|
22
|
+
execute_safely(callback, context)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Execute callback with error isolation and arity handling
|
|
26
|
+
#
|
|
27
|
+
# @param callback [Proc] The callback to execute
|
|
28
|
+
# @param context [CallbackContext] The context to pass
|
|
29
|
+
def execute_safely(callback, context)
|
|
30
|
+
case callback.arity
|
|
31
|
+
when 1, -1, -2 # Accepts one arg or variable args
|
|
32
|
+
callback.call(context)
|
|
33
|
+
when 0
|
|
34
|
+
callback.call
|
|
35
|
+
else
|
|
36
|
+
log_warn "[UsageCredits] Callback has unexpected arity (#{callback.arity}). Expected 0 or 1."
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
# Log but don't re-raise - callbacks should never break credit operations
|
|
40
|
+
log_error "[UsageCredits] Callback error for #{context.event}: #{e.class}: #{e.message}"
|
|
41
|
+
log_debug e.backtrace.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Safe logging that works with or without Rails
|
|
45
|
+
def log_error(message)
|
|
46
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
47
|
+
Rails.logger.error(message)
|
|
48
|
+
else
|
|
49
|
+
warn message
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def log_warn(message)
|
|
54
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
55
|
+
Rails.logger.warn(message)
|
|
56
|
+
else
|
|
57
|
+
warn message
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def log_debug(message)
|
|
62
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger&.debug?
|
|
63
|
+
Rails.logger.debug(message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -45,6 +45,18 @@ module UsageCredits
|
|
|
45
45
|
|
|
46
46
|
attr_reader :low_balance_callback
|
|
47
47
|
|
|
48
|
+
# =========================================
|
|
49
|
+
# Lifecycle Callbacks
|
|
50
|
+
# =========================================
|
|
51
|
+
|
|
52
|
+
attr_reader :on_credits_added_callback,
|
|
53
|
+
:on_credits_deducted_callback,
|
|
54
|
+
:on_low_balance_reached_callback,
|
|
55
|
+
:on_balance_depleted_callback,
|
|
56
|
+
:on_insufficient_credits_callback,
|
|
57
|
+
:on_subscription_credits_awarded_callback,
|
|
58
|
+
:on_credit_pack_purchased_callback
|
|
59
|
+
|
|
48
60
|
def initialize
|
|
49
61
|
# Initialize empty data stores
|
|
50
62
|
@operations = {} # Credit-consuming operations (e.g., "send_email: 1 credit")
|
|
@@ -71,6 +83,15 @@ module UsageCredits
|
|
|
71
83
|
@allow_negative_balance = false
|
|
72
84
|
@low_balance_threshold = nil
|
|
73
85
|
@low_balance_callback = nil # Called when user hits low_balance_threshold
|
|
86
|
+
|
|
87
|
+
# Lifecycle callbacks (all nil by default)
|
|
88
|
+
@on_credits_added_callback = nil
|
|
89
|
+
@on_credits_deducted_callback = nil
|
|
90
|
+
@on_low_balance_reached_callback = nil
|
|
91
|
+
@on_balance_depleted_callback = nil
|
|
92
|
+
@on_insufficient_credits_callback = nil
|
|
93
|
+
@on_subscription_credits_awarded_callback = nil
|
|
94
|
+
@on_credit_pack_purchased_callback = nil
|
|
74
95
|
end
|
|
75
96
|
|
|
76
97
|
# =========================================
|
|
@@ -189,10 +210,55 @@ module UsageCredits
|
|
|
189
210
|
@credit_formatter = block
|
|
190
211
|
end
|
|
191
212
|
|
|
192
|
-
#
|
|
213
|
+
# =========================================
|
|
214
|
+
# Lifecycle Callback DSL Methods
|
|
215
|
+
# =========================================
|
|
216
|
+
# All methods allow nil block to clear the callback (useful for testing)
|
|
217
|
+
|
|
218
|
+
# Called after credits are added to a wallet
|
|
219
|
+
def on_credits_added(&block)
|
|
220
|
+
@on_credits_added_callback = block
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Called after credits are deducted from a wallet
|
|
224
|
+
def on_credits_deducted(&block)
|
|
225
|
+
@on_credits_deducted_callback = block
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Called when balance crosses below the low_balance_threshold
|
|
229
|
+
# Receives CallbackContext with full event data
|
|
230
|
+
def on_low_balance_reached(&block)
|
|
231
|
+
@on_low_balance_reached_callback = block
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Called when balance reaches exactly zero
|
|
235
|
+
def on_balance_depleted(&block)
|
|
236
|
+
@on_balance_depleted_callback = block
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Called when an operation fails due to insufficient credits
|
|
240
|
+
def on_insufficient_credits(&block)
|
|
241
|
+
@on_insufficient_credits_callback = block
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Called after subscription credits are awarded
|
|
245
|
+
def on_subscription_credits_awarded(&block)
|
|
246
|
+
@on_subscription_credits_awarded_callback = block
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Called after a credit pack is purchased
|
|
250
|
+
def on_credit_pack_purchased(&block)
|
|
251
|
+
@on_credit_pack_purchased_callback = block
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# BACKWARD COMPATIBILITY: Legacy method that receives owner, not context
|
|
255
|
+
# Existing users' code: config.on_low_balance { |owner| ... }
|
|
193
256
|
def on_low_balance(&block)
|
|
194
257
|
raise ArgumentError, "Block is required for low balance callback" unless block_given?
|
|
258
|
+
# Store legacy callback as before (for backward compat with direct calls)
|
|
195
259
|
@low_balance_callback = block
|
|
260
|
+
# Also create a wrapper for new callback system that extracts owner from context
|
|
261
|
+
@on_low_balance_reached_callback = ->(ctx) { block.call(ctx.owner) }
|
|
196
262
|
end
|
|
197
263
|
|
|
198
264
|
# =========================================
|
|
@@ -23,14 +23,21 @@ module UsageCredits
|
|
|
23
23
|
def succeeded?
|
|
24
24
|
case type
|
|
25
25
|
when "Pay::Stripe::Charge"
|
|
26
|
-
|
|
26
|
+
# Pay 10+ stores the full Stripe charge object in `object` column
|
|
27
|
+
# Older Pay versions stored charge details in `data` column
|
|
28
|
+
# We check both for backward compatibility
|
|
29
|
+
stripe_object = charge_object_data
|
|
30
|
+
status = stripe_object["status"]
|
|
31
|
+
|
|
27
32
|
# Explicitly check for failure states
|
|
28
33
|
return false if status == "failed"
|
|
29
34
|
return false if status == "pending"
|
|
30
35
|
return false if status == "canceled"
|
|
31
36
|
return true if status == "succeeded"
|
|
37
|
+
|
|
32
38
|
# Fallback: check if amount was actually captured
|
|
33
|
-
|
|
39
|
+
amount_captured = stripe_object["amount_captured"].to_i
|
|
40
|
+
return amount_captured == amount.to_i && amount.to_i.positive?
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
# For non-Stripe charges, we assume Pay only creates charges after successful payment
|
|
@@ -39,6 +46,20 @@ module UsageCredits
|
|
|
39
46
|
true
|
|
40
47
|
end
|
|
41
48
|
|
|
49
|
+
# Returns the Stripe charge object data, checking both `object` (Pay 10+)
|
|
50
|
+
# and `data` (older Pay versions) for backward compatibility
|
|
51
|
+
def charge_object_data
|
|
52
|
+
# Pay 10+ stores full Stripe object in `object` column
|
|
53
|
+
if respond_to?(:object) && object.is_a?(Hash) && object.any?
|
|
54
|
+
object.with_indifferent_access
|
|
55
|
+
# Older Pay versions stored charge details in `data` column
|
|
56
|
+
elsif data.is_a?(Hash) && data.any?
|
|
57
|
+
data.with_indifferent_access
|
|
58
|
+
else
|
|
59
|
+
{}.with_indifferent_access
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
42
63
|
def refunded?
|
|
43
64
|
return false unless amount_refunded
|
|
44
65
|
amount_refunded > 0
|
|
@@ -136,11 +157,13 @@ module UsageCredits
|
|
|
136
157
|
end
|
|
137
158
|
|
|
138
159
|
begin
|
|
160
|
+
credit_transaction = nil
|
|
161
|
+
|
|
139
162
|
# Wrap in transaction to ensure atomicity - if Fulfillment.create! fails,
|
|
140
163
|
# the credits should NOT be added. This is critical for money handling.
|
|
141
164
|
ActiveRecord::Base.transaction do
|
|
142
165
|
# Add credits to the user's wallet
|
|
143
|
-
credit_wallet.add_credits(
|
|
166
|
+
credit_transaction = credit_wallet.add_credits(
|
|
144
167
|
pack.total_credits,
|
|
145
168
|
category: "credit_pack_purchase",
|
|
146
169
|
metadata: {
|
|
@@ -169,6 +192,20 @@ module UsageCredits
|
|
|
169
192
|
)
|
|
170
193
|
end
|
|
171
194
|
|
|
195
|
+
# Dispatch credit_pack_purchased callback after successful fulfillment
|
|
196
|
+
# Note: credits_added callback was already fired by add_credits
|
|
197
|
+
UsageCredits::Callbacks.dispatch(:credit_pack_purchased,
|
|
198
|
+
wallet: credit_wallet,
|
|
199
|
+
amount: pack.total_credits,
|
|
200
|
+
transaction: credit_transaction,
|
|
201
|
+
metadata: {
|
|
202
|
+
credit_pack_name: pack_name,
|
|
203
|
+
credit_pack: pack,
|
|
204
|
+
pay_charge: self,
|
|
205
|
+
price_cents: pack.price_cents
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
172
209
|
Rails.logger.info "Successfully fulfilled credit pack #{pack_name} for charge #{id}"
|
|
173
210
|
rescue StandardError => e
|
|
174
211
|
Rails.logger.error "Failed to fulfill credit pack #{pack_name} for charge #{id}: #{e.message}"
|
|
@@ -162,17 +162,20 @@ module UsageCredits
|
|
|
162
162
|
Rails.logger.info " Status: #{status}"
|
|
163
163
|
Rails.logger.info " Plan: #{plan}"
|
|
164
164
|
|
|
165
|
+
# Variables to track for callback dispatch after transaction commits
|
|
166
|
+
total_credits_awarded = 0
|
|
167
|
+
last_credit_transaction = nil
|
|
168
|
+
|
|
165
169
|
# Transaction for atomic awarding + fulfillment creation/reactivation
|
|
170
|
+
# Callback is dispatched AFTER this block to ensure credits are persisted
|
|
166
171
|
ActiveRecord::Base.transaction do
|
|
167
|
-
|
|
168
|
-
total_credits_awarded = 0
|
|
169
172
|
transaction_ids = []
|
|
170
173
|
|
|
171
174
|
# 1) If this is a trial and not an active subscription: award trial credits, if any
|
|
172
175
|
if status == "trialing" && plan.trial_credits.positive?
|
|
173
176
|
|
|
174
177
|
# Immediate awarding of trial credits
|
|
175
|
-
|
|
178
|
+
last_credit_transaction = wallet.add_credits(plan.trial_credits,
|
|
176
179
|
category: "subscription_trial",
|
|
177
180
|
expires_at: trial_ends_at,
|
|
178
181
|
metadata: {
|
|
@@ -182,14 +185,14 @@ module UsageCredits
|
|
|
182
185
|
fulfilled_at: Time.current
|
|
183
186
|
}
|
|
184
187
|
)
|
|
185
|
-
transaction_ids <<
|
|
188
|
+
transaction_ids << last_credit_transaction.id
|
|
186
189
|
total_credits_awarded += plan.trial_credits
|
|
187
190
|
|
|
188
191
|
elsif status == "active"
|
|
189
192
|
|
|
190
193
|
# Awarding of signup bonus, if any (only on initial setup, not reactivation)
|
|
191
194
|
if plan.signup_bonus_credits.positive? && !is_reactivation
|
|
192
|
-
|
|
195
|
+
bonus_transaction = wallet.add_credits(plan.signup_bonus_credits,
|
|
193
196
|
category: "subscription_signup_bonus",
|
|
194
197
|
metadata: {
|
|
195
198
|
subscription_id: id,
|
|
@@ -198,13 +201,14 @@ module UsageCredits
|
|
|
198
201
|
fulfilled_at: Time.current
|
|
199
202
|
}
|
|
200
203
|
)
|
|
201
|
-
transaction_ids <<
|
|
204
|
+
transaction_ids << bonus_transaction.id
|
|
202
205
|
total_credits_awarded += plan.signup_bonus_credits
|
|
206
|
+
last_credit_transaction = bonus_transaction
|
|
203
207
|
end
|
|
204
208
|
|
|
205
209
|
# Actual awarding of the subscription credits
|
|
206
210
|
if plan.credits_per_period.positive?
|
|
207
|
-
|
|
211
|
+
credits_transaction = wallet.add_credits(plan.credits_per_period,
|
|
208
212
|
category: "subscription_credits",
|
|
209
213
|
expires_at: credits_expire_at, # This will be nil if credit rollover is enabled
|
|
210
214
|
metadata: {
|
|
@@ -214,8 +218,9 @@ module UsageCredits
|
|
|
214
218
|
fulfilled_at: Time.current
|
|
215
219
|
}
|
|
216
220
|
)
|
|
217
|
-
transaction_ids <<
|
|
221
|
+
transaction_ids << credits_transaction.id
|
|
218
222
|
total_credits_awarded += plan.credits_per_period
|
|
223
|
+
last_credit_transaction = credits_transaction
|
|
219
224
|
end
|
|
220
225
|
end
|
|
221
226
|
|
|
@@ -249,13 +254,12 @@ module UsageCredits
|
|
|
249
254
|
"reactivated_at" => Time.current
|
|
250
255
|
)
|
|
251
256
|
)
|
|
252
|
-
fulfillment = existing_reactivatable_fulfillment
|
|
253
257
|
|
|
254
|
-
Rails.logger.info "Reactivated fulfillment #{
|
|
258
|
+
Rails.logger.info "Reactivated fulfillment #{existing_reactivatable_fulfillment.id} for subscription #{id}"
|
|
255
259
|
else
|
|
256
260
|
# Create new fulfillment
|
|
257
261
|
# Use string keys consistently to avoid duplicates after JSON serialization
|
|
258
|
-
|
|
262
|
+
UsageCredits::Fulfillment.create!(
|
|
259
263
|
wallet: wallet,
|
|
260
264
|
source: self,
|
|
261
265
|
fulfillment_type: "subscription",
|
|
@@ -270,16 +274,34 @@ module UsageCredits
|
|
|
270
274
|
}
|
|
271
275
|
)
|
|
272
276
|
|
|
273
|
-
Rails.logger.info "Initial fulfillment for subscription #{id} finished
|
|
277
|
+
Rails.logger.info "Initial fulfillment for subscription #{id} finished"
|
|
274
278
|
end
|
|
275
279
|
|
|
276
280
|
# Link created transactions to the fulfillment object for traceability
|
|
277
|
-
UsageCredits::
|
|
281
|
+
fulfillment_record = UsageCredits::Fulfillment.find_by(source: self)
|
|
282
|
+
UsageCredits::Transaction.where(id: transaction_ids).update_all(fulfillment_id: fulfillment_record&.id) if transaction_ids.any?
|
|
283
|
+
end
|
|
278
284
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
# Dispatch callback AFTER transaction commits - ensures credits are persisted
|
|
286
|
+
if total_credits_awarded > 0
|
|
287
|
+
UsageCredits::Callbacks.dispatch(:subscription_credits_awarded,
|
|
288
|
+
wallet: wallet,
|
|
289
|
+
amount: total_credits_awarded,
|
|
290
|
+
transaction: last_credit_transaction,
|
|
291
|
+
metadata: {
|
|
292
|
+
subscription_plan_name: plan.name,
|
|
293
|
+
subscription: plan,
|
|
294
|
+
pay_subscription: self,
|
|
295
|
+
fulfillment_period: plan.fulfillment_period_display,
|
|
296
|
+
is_reactivation: is_reactivation,
|
|
297
|
+
status: status
|
|
298
|
+
}
|
|
299
|
+
)
|
|
282
300
|
end
|
|
301
|
+
|
|
302
|
+
rescue => e
|
|
303
|
+
Rails.logger.error "Failed to fulfill initial credits for subscription #{id}: #{e.message}"
|
|
304
|
+
raise
|
|
283
305
|
end
|
|
284
306
|
|
|
285
307
|
# Handle subscription renewal (we received a new payment for another billing period)
|
|
@@ -433,45 +455,64 @@ module UsageCredits
|
|
|
433
455
|
|
|
434
456
|
Rails.logger.info " [UPGRADE] Credits expire at: #{credits_expire_at || 'never (rollover enabled)'}"
|
|
435
457
|
|
|
436
|
-
# Grant full new plan credits immediately
|
|
437
|
-
# Use string keys consistently to avoid duplicates after JSON serialization
|
|
438
|
-
wallet.add_credits(
|
|
439
|
-
new_plan.credits_per_period,
|
|
440
|
-
category: "subscription_upgrade",
|
|
441
|
-
expires_at: credits_expire_at,
|
|
442
|
-
metadata: {
|
|
443
|
-
"subscription_id" => id,
|
|
444
|
-
"plan" => processor_plan,
|
|
445
|
-
"reason" => "plan_upgrade",
|
|
446
|
-
"fulfilled_at" => Time.current
|
|
447
|
-
}
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
Rails.logger.info " [UPGRADE] Credits awarded successfully"
|
|
451
|
-
Rails.logger.info " [UPGRADE] New balance: #{wallet.reload.balance}"
|
|
452
|
-
|
|
453
458
|
# Calculate next fulfillment time based on the NEW plan's period
|
|
454
459
|
# This ensures the fulfillment schedule matches the new plan's cadence
|
|
455
460
|
next_fulfillment_at = Time.current + new_plan.parsed_fulfillment_period
|
|
456
461
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
Rails.logger.info " [UPGRADE] Old next_fulfillment_at: #{fulfillment.next_fulfillment_at}"
|
|
461
|
-
Rails.logger.info " [UPGRADE] New next_fulfillment_at: #{next_fulfillment_at}"
|
|
462
|
+
# Wrap all database operations in a transaction to ensure atomicity
|
|
463
|
+
# The callback should only fire after ALL operations succeed
|
|
464
|
+
upgrade_transaction = nil
|
|
462
465
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
466
|
+
ActiveRecord::Base.transaction do
|
|
467
|
+
# Grant full new plan credits immediately
|
|
468
|
+
# Use string keys consistently to avoid duplicates after JSON serialization
|
|
469
|
+
upgrade_transaction = wallet.add_credits(
|
|
470
|
+
new_plan.credits_per_period,
|
|
471
|
+
category: "subscription_upgrade",
|
|
472
|
+
expires_at: credits_expire_at,
|
|
473
|
+
metadata: {
|
|
474
|
+
"subscription_id" => id,
|
|
475
|
+
"plan" => processor_plan,
|
|
476
|
+
"reason" => "plan_upgrade",
|
|
477
|
+
"fulfilled_at" => Time.current
|
|
478
|
+
}
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
Rails.logger.info " [UPGRADE] Updating fulfillment record"
|
|
482
|
+
Rails.logger.info " [UPGRADE] Old fulfillment_period: #{fulfillment.fulfillment_period}"
|
|
483
|
+
Rails.logger.info " [UPGRADE] New fulfillment_period: #{new_plan.fulfillment_period_display}"
|
|
484
|
+
Rails.logger.info " [UPGRADE] Old next_fulfillment_at: #{fulfillment.next_fulfillment_at}"
|
|
485
|
+
Rails.logger.info " [UPGRADE] New next_fulfillment_at: #{next_fulfillment_at}"
|
|
486
|
+
|
|
487
|
+
# Update fulfillment with ALL new plan properties
|
|
488
|
+
# This includes the period display string and the next fulfillment time
|
|
489
|
+
# to ensure future fulfillments happen on the correct schedule
|
|
490
|
+
# Use string keys consistently to avoid duplicates after JSON serialization
|
|
491
|
+
fulfillment.update!(
|
|
492
|
+
fulfillment_period: new_plan.fulfillment_period_display,
|
|
493
|
+
next_fulfillment_at: next_fulfillment_at,
|
|
494
|
+
metadata: fulfillment.metadata
|
|
495
|
+
.except("pending_plan_change", "plan_change_at")
|
|
496
|
+
.merge("plan" => processor_plan)
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Dispatch callback AFTER transaction commits - ensures credits are persisted
|
|
501
|
+
UsageCredits::Callbacks.dispatch(:subscription_credits_awarded,
|
|
502
|
+
wallet: wallet,
|
|
503
|
+
amount: new_plan.credits_per_period,
|
|
504
|
+
transaction: upgrade_transaction,
|
|
505
|
+
metadata: {
|
|
506
|
+
subscription_plan_name: new_plan.name,
|
|
507
|
+
subscription: new_plan,
|
|
508
|
+
pay_subscription: self,
|
|
509
|
+
fulfillment_period: new_plan.fulfillment_period_display,
|
|
510
|
+
reason: "plan_upgrade"
|
|
511
|
+
}
|
|
473
512
|
)
|
|
474
513
|
|
|
514
|
+
Rails.logger.info " [UPGRADE] Credits awarded successfully"
|
|
515
|
+
Rails.logger.info " [UPGRADE] New balance: #{wallet.reload.balance}"
|
|
475
516
|
Rails.logger.info " [UPGRADE] Fulfillment updated successfully"
|
|
476
517
|
Rails.logger.info "Subscription #{id} upgraded to #{processor_plan}, granted #{new_plan.credits_per_period} credits"
|
|
477
518
|
Rails.logger.info " Fulfillment period updated to: #{new_plan.fulfillment_period_display}"
|
|
@@ -91,7 +91,20 @@ module UsageCredits
|
|
|
91
91
|
cost = operation.calculate_cost(params)
|
|
92
92
|
|
|
93
93
|
# Check if user has enough credits
|
|
94
|
-
|
|
94
|
+
unless has_enough_credits_to?(operation_name, **params)
|
|
95
|
+
# Fire insufficient_credits callback before raising
|
|
96
|
+
UsageCredits::Callbacks.dispatch(:insufficient_credits,
|
|
97
|
+
wallet: self,
|
|
98
|
+
amount: cost,
|
|
99
|
+
operation_name: operation_name,
|
|
100
|
+
metadata: {
|
|
101
|
+
available: credits,
|
|
102
|
+
required: cost,
|
|
103
|
+
params: params
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
raise InsufficientCredits, "Insufficient credits (#{credits} < #{cost})"
|
|
107
|
+
end
|
|
95
108
|
|
|
96
109
|
# Create audit trail
|
|
97
110
|
# Stringify keys from audit_data to avoid duplicate key warnings in JSON
|
|
@@ -156,6 +169,8 @@ module UsageCredits
|
|
|
156
169
|
amount = amount.to_i
|
|
157
170
|
raise ArgumentError, "Cannot add non-positive credits" if amount <= 0
|
|
158
171
|
|
|
172
|
+
previous_balance = credits # Capture BEFORE creating transaction
|
|
173
|
+
|
|
159
174
|
transaction = transactions.create!(
|
|
160
175
|
amount: amount,
|
|
161
176
|
category: category,
|
|
@@ -168,7 +183,16 @@ module UsageCredits
|
|
|
168
183
|
self.balance = credits
|
|
169
184
|
save!
|
|
170
185
|
|
|
171
|
-
|
|
186
|
+
# Dispatch callback with full context
|
|
187
|
+
UsageCredits::Callbacks.dispatch(:credits_added,
|
|
188
|
+
wallet: self,
|
|
189
|
+
amount: amount,
|
|
190
|
+
category: category,
|
|
191
|
+
transaction: transaction,
|
|
192
|
+
previous_balance: previous_balance,
|
|
193
|
+
new_balance: balance,
|
|
194
|
+
metadata: metadata
|
|
195
|
+
)
|
|
172
196
|
|
|
173
197
|
# To finish, let's return the transaction that has been just created so we can reference it in parts of the code
|
|
174
198
|
# Useful, for example, to update the transaction's `fulfillment` reference in the subscription extension
|
|
@@ -253,11 +277,35 @@ module UsageCredits
|
|
|
253
277
|
self.balance = credits
|
|
254
278
|
save!
|
|
255
279
|
|
|
256
|
-
#
|
|
257
|
-
|
|
280
|
+
# Dispatch credits_deducted callback
|
|
281
|
+
UsageCredits::Callbacks.dispatch(:credits_deducted,
|
|
282
|
+
wallet: self,
|
|
283
|
+
amount: amount,
|
|
284
|
+
category: category,
|
|
285
|
+
transaction: spend_tx,
|
|
286
|
+
previous_balance: previous_balance,
|
|
287
|
+
new_balance: balance,
|
|
288
|
+
metadata: metadata
|
|
289
|
+
)
|
|
258
290
|
|
|
259
|
-
# Check
|
|
260
|
-
|
|
291
|
+
# Check for low balance threshold crossing
|
|
292
|
+
if !was_low_balance?(previous_balance) && low_balance?
|
|
293
|
+
UsageCredits::Callbacks.dispatch(:low_balance_reached,
|
|
294
|
+
wallet: self,
|
|
295
|
+
threshold: UsageCredits.configuration.low_balance_threshold,
|
|
296
|
+
previous_balance: previous_balance,
|
|
297
|
+
new_balance: balance
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Check for balance depletion (balance reaches exactly zero)
|
|
302
|
+
if previous_balance > 0 && balance == 0
|
|
303
|
+
UsageCredits::Callbacks.dispatch(:balance_depleted,
|
|
304
|
+
wallet: self,
|
|
305
|
+
previous_balance: previous_balance,
|
|
306
|
+
new_balance: 0
|
|
307
|
+
)
|
|
308
|
+
end
|
|
261
309
|
|
|
262
310
|
spend_tx
|
|
263
311
|
end
|
|
@@ -291,23 +339,9 @@ module UsageCredits
|
|
|
291
339
|
end
|
|
292
340
|
|
|
293
341
|
# =========================================
|
|
294
|
-
# Balance
|
|
342
|
+
# Balance Threshold Helpers
|
|
295
343
|
# =========================================
|
|
296
344
|
|
|
297
|
-
def notify_balance_change(event, amount)
|
|
298
|
-
UsageCredits.handle_event(
|
|
299
|
-
event,
|
|
300
|
-
wallet: self,
|
|
301
|
-
amount: amount,
|
|
302
|
-
balance: credits
|
|
303
|
-
)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def check_low_balance
|
|
307
|
-
return unless low_balance?
|
|
308
|
-
UsageCredits.handle_event(:low_balance_reached, wallet: self)
|
|
309
|
-
end
|
|
310
|
-
|
|
311
345
|
def low_balance?
|
|
312
346
|
threshold = UsageCredits.configuration.low_balance_threshold
|
|
313
347
|
return false if threshold.nil? || threshold.negative?
|
data/lib/usage_credits.rb
CHANGED
|
@@ -29,6 +29,8 @@ require "usage_credits/models/concerns/pay_charge_extension"
|
|
|
29
29
|
# 4. Core functionality
|
|
30
30
|
require "usage_credits/version"
|
|
31
31
|
require "usage_credits/configuration" # Single source of truth for all configuration in this gem
|
|
32
|
+
require "usage_credits/callback_context" # Struct for callback event data
|
|
33
|
+
require "usage_credits/callbacks" # Callback dispatch module
|
|
32
34
|
|
|
33
35
|
# 5. Shim Rails classes so requires don't break
|
|
34
36
|
module UsageCredits
|
|
@@ -76,6 +78,7 @@ module UsageCredits
|
|
|
76
78
|
# Reset configuration to defaults (mainly for testing)
|
|
77
79
|
def reset!
|
|
78
80
|
@configuration = nil
|
|
81
|
+
@deprecation_warnings = {}
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
# DSL methods - all delegate to configuration
|
|
@@ -128,16 +131,30 @@ module UsageCredits
|
|
|
128
131
|
end
|
|
129
132
|
alias_method :find_plan_by_id, :find_subscription_plan_by_processor_id
|
|
130
133
|
|
|
131
|
-
# Event handling for low balance notifications
|
|
134
|
+
# DEPRECATED: Event handling for low balance notifications
|
|
135
|
+
# Use on_low_balance_reached callback instead
|
|
136
|
+
# This method shows a deprecation warning only once to avoid log spam
|
|
132
137
|
def notify_low_balance(owner)
|
|
138
|
+
@deprecation_warnings ||= {}
|
|
139
|
+
unless @deprecation_warnings[:notify_low_balance]
|
|
140
|
+
warn "[DEPRECATION] UsageCredits.notify_low_balance is deprecated. Use on_low_balance_reached callback instead."
|
|
141
|
+
@deprecation_warnings[:notify_low_balance] = true
|
|
142
|
+
end
|
|
133
143
|
return unless configuration.low_balance_callback
|
|
134
144
|
configuration.low_balance_callback.call(owner)
|
|
135
145
|
end
|
|
136
146
|
|
|
147
|
+
# DEPRECATED: Events are now dispatched through Callbacks module
|
|
148
|
+
# This method shows a deprecation warning only once to avoid log spam
|
|
137
149
|
def handle_event(event, **params)
|
|
150
|
+
@deprecation_warnings ||= {}
|
|
151
|
+
unless @deprecation_warnings[:handle_event]
|
|
152
|
+
warn "[DEPRECATION] UsageCredits.handle_event is deprecated. Events are now dispatched through the Callbacks module."
|
|
153
|
+
@deprecation_warnings[:handle_event] = true
|
|
154
|
+
end
|
|
138
155
|
case event
|
|
139
156
|
when :low_balance_reached
|
|
140
|
-
notify_low_balance(params[:wallet]
|
|
157
|
+
notify_low_balance(params[:wallet]&.owner)
|
|
141
158
|
end
|
|
142
159
|
end
|
|
143
160
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: usage_credits
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
@@ -74,6 +74,8 @@ files:
|
|
|
74
74
|
- lib/generators/usage_credits/templates/create_usage_credits_tables.rb.erb
|
|
75
75
|
- lib/generators/usage_credits/templates/initializer.rb
|
|
76
76
|
- lib/usage_credits.rb
|
|
77
|
+
- lib/usage_credits/callback_context.rb
|
|
78
|
+
- lib/usage_credits/callbacks.rb
|
|
77
79
|
- lib/usage_credits/configuration.rb
|
|
78
80
|
- lib/usage_credits/core_ext/numeric.rb
|
|
79
81
|
- lib/usage_credits/cost/base.rb
|