usage_credits 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fcda02fd1ae61887b8a80d6fbf6c864843d363797d90dbbd66abafe2851ce8b
4
- data.tar.gz: 6c1e67ec12f46bf5e33faa7d02ed723dc47926a29948a8140706392d1ffbe024
3
+ metadata.gz: 8e1f36f964eea89e835d789f34b6b2ce8994940896d21652af2400282c0e24a0
4
+ data.tar.gz: ad7c6be299aee7f89929841bfe18a6f44e2dd91361afe30ff238980bdc75544b
5
5
  SHA512:
6
- metadata.gz: cd9d8517d94b2c19d0b141d9b47f4362c65ee1bf9255bff400c2b55c34b14a87903cdd3f58987e92edad8c2116fbfe485f185ddc2e9696782ae912fc7f40402c
7
- data.tar.gz: b9498add88988145a75e74abbdf24b086391fd76188d63719a108a97f4b1fa69af86d9ea9c941cdd45ac25a2d1e5319192b14d34361a2ea42058db333f6adb0e
6
+ metadata.gz: 4047f5a333b5e1359468468927100ea71a5c5b7117d8eec9e73a81ad8b40c243d796dd3d0e37604c9fd46b9b44239612d5bc1b30176e4322364312cc1c900017
7
+ data.tar.gz: b21b55c0b524b18fcf47339b926a425f53fcca5a3148f221bc8e86e4976f1281425041edd1d9cc903b68ab6cf82dd38a1a97df84373d1658d021cc43b3308de0
data/Appraisals CHANGED
@@ -1,25 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Test against Pay 8.3.x (minimum supported version)
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)
4
18
  appraise "pay-8.3" do
5
19
  gem "pay", "~> 8.3.0"
6
20
  gem "stripe", "~> 13.0"
21
+ gem "rails", "~> 8.1.0"
7
22
  end
8
23
 
9
- # Test against Pay 9.0.x (current recommended version)
24
+ # Test Pay 9.0 (popular stable version with latest Rails)
10
25
  appraise "pay-9.0" do
11
26
  gem "pay", "~> 9.0.0"
12
27
  gem "stripe", "~> 13.0"
28
+ gem "rails", "~> 8.1.0"
13
29
  end
14
30
 
15
- # Test against Pay 10.x (newly supported version)
31
+ # Test Pay 10.0 (with latest Rails)
16
32
  appraise "pay-10.0" do
17
33
  gem "pay", "~> 10.0.0"
18
34
  gem "stripe", "~> 15.0"
35
+ gem "rails", "~> 8.1.0"
19
36
  end
20
37
 
21
- # Test against Pay 11.x (latest version)
38
+ # Test latest Pay version (with latest Rails)
22
39
  appraise "pay-11.0" do
23
40
  gem "pay", "~> 11.0"
24
41
  gem "stripe", "~> 18.0"
42
+ gem "rails", "~> 8.1.0"
25
43
  end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
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
+
6
+ ## [0.2.1] - 2026-01-15
7
+
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
9
+
1
10
  ## [0.2.0] - 2025-12-29
2
11
 
3
12
  - Add Claude Code GitHub Workflow by @rameerez in https://github.com/rameerez/usage_credits/pull/14
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # 💳✨ `usage_credits` - Add usage-based credits to your Rails app
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/usage_credits.svg?v=0.1)](https://badge.fury.io/rb/usage_credits?v=0.1)
3
+ [![Gem Version](https://badge.fury.io/rb/usage_credits.svg)](https://badge.fury.io/rb/usage_credits) [![Build Status](https://github.com/rameerez/usage_credits/workflows/Tests/badge.svg)](https://github.com/rameerez/usage_credits/actions)
4
4
 
5
- Allow your users to have in-app credits / tokens they can use to perform operations.
5
+ > [!TIP]
6
+ > **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks.
7
+
8
+ `usage_credits` allows your users to have in-app credits / tokens they can use to perform operations.
6
9
 
7
10
  ✨ Perfect for SaaS, AI apps, games, and API products that want to implement usage-based pricing.
8
11
 
@@ -262,27 +265,83 @@ process_image(params) # If this fails, credits are already spent!
262
265
  > If validation fails (e.g., file too large), both methods will raise `InvalidOperation`.
263
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.
264
267
 
265
- ## Low balance alerts
268
+ ## Low balance alerts & other lifecycle callbacks
269
+
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
266
295
 
267
- You can hook on to our low balance event to notify users when they are running low on credits (useful to upsell them a credit pack):
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:
268
306
 
269
307
  ```ruby
270
308
  UsageCredits.configure do |config|
271
- # Alert when balance drops below 100 credits
272
- # Set to nil to disable low balance alerts
273
- config.low_balance_threshold = 100.credits
274
-
275
- # Handle low credit balance alerts
276
- config.on_low_balance do |user|
277
- # Send notification to user
278
- UserMailer.low_credits_alert(user).deliver_later
279
-
280
- # Or trigger any other business logic
281
- SlackNotifier.notify("User #{user.id} is running low on credits!")
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"
282
324
  end
283
325
  end
284
326
  ```
285
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
+
286
345
  ## Award bonus credits
287
346
 
288
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.
@@ -2,28 +2,40 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal"
6
- gem "mocha"
7
- gem "simplecov", require: false
8
- gem "standard"
9
- gem "vcr"
10
- gem "webmock"
11
- gem "braintree", ">= 2.92.0"
12
- gem "lemonsqueezy", "~> 1.0"
13
- gem "paddle", "~> 2.6"
14
- gem "stripe", "~> 15.0"
15
- gem "prawn"
16
- gem "receipts"
17
- gem "sqlite3"
18
- gem "pg"
19
- gem "bootsnap", require: false
20
- gem "puma"
21
- gem "web-console", group: :development
22
- gem "importmap-rails"
23
- gem "sprockets-rails"
24
- gem "stimulus-rails"
25
- gem "turbo-rails"
26
- gem "rdoc", ">= 7.0"
5
+ gem "rake", "~> 13.0"
27
6
  gem "pay", "~> 10.0.0"
7
+ gem "stripe", "~> 15.0"
8
+ gem "rails", "~> 8.1.0"
9
+
10
+ group :development do
11
+ gem "appraisal"
12
+ gem "web-console"
13
+ gem "standard"
14
+ gem "rubocop", "~> 1.0"
15
+ gem "rubocop-minitest", "~> 0.35"
16
+ gem "rubocop-performance", "~> 1.0"
17
+ end
18
+
19
+ group :test do
20
+ gem "minitest", "~> 5.0"
21
+ gem "mocha"
22
+ gem "simplecov", require: false
23
+ gem "vcr"
24
+ gem "webmock"
25
+ gem "braintree", ">= 2.92.0"
26
+ gem "lemonsqueezy", "~> 1.0"
27
+ gem "paddle", "~> 2.6"
28
+ gem "prawn"
29
+ gem "receipts"
30
+ gem "sqlite3"
31
+ gem "pg"
32
+ gem "bootsnap", require: false
33
+ gem "puma"
34
+ gem "importmap-rails"
35
+ gem "sprockets-rails"
36
+ gem "stimulus-rails"
37
+ gem "turbo-rails"
38
+ gem "rdoc", ">= 7.0"
39
+ end
28
40
 
29
41
  gemspec path: "../"
@@ -2,28 +2,40 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal"
6
- gem "mocha"
7
- gem "simplecov", require: false
8
- gem "standard"
9
- gem "vcr"
10
- gem "webmock"
11
- gem "braintree", ">= 2.92.0"
12
- gem "lemonsqueezy", "~> 1.0"
13
- gem "paddle", "~> 2.6"
14
- gem "stripe", "~> 18.0"
15
- gem "prawn"
16
- gem "receipts"
17
- gem "sqlite3"
18
- gem "pg"
19
- gem "bootsnap", require: false
20
- gem "puma"
21
- gem "web-console", group: :development
22
- gem "importmap-rails"
23
- gem "sprockets-rails"
24
- gem "stimulus-rails"
25
- gem "turbo-rails"
26
- gem "rdoc", ">= 7.0"
5
+ gem "rake", "~> 13.0"
27
6
  gem "pay", "~> 11.0"
7
+ gem "stripe", "~> 18.0"
8
+ gem "rails", "~> 8.1.0"
9
+
10
+ group :development do
11
+ gem "appraisal"
12
+ gem "web-console"
13
+ gem "standard"
14
+ gem "rubocop", "~> 1.0"
15
+ gem "rubocop-minitest", "~> 0.35"
16
+ gem "rubocop-performance", "~> 1.0"
17
+ end
18
+
19
+ group :test do
20
+ gem "minitest", "~> 5.0"
21
+ gem "mocha"
22
+ gem "simplecov", require: false
23
+ gem "vcr"
24
+ gem "webmock"
25
+ gem "braintree", ">= 2.92.0"
26
+ gem "lemonsqueezy", "~> 1.0"
27
+ gem "paddle", "~> 2.6"
28
+ gem "prawn"
29
+ gem "receipts"
30
+ gem "sqlite3"
31
+ gem "pg"
32
+ gem "bootsnap", require: false
33
+ gem "puma"
34
+ gem "importmap-rails"
35
+ gem "sprockets-rails"
36
+ gem "stimulus-rails"
37
+ gem "turbo-rails"
38
+ gem "rdoc", ">= 7.0"
39
+ end
28
40
 
29
41
  gemspec path: "../"
@@ -2,28 +2,40 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal"
6
- gem "mocha"
7
- gem "simplecov", require: false
8
- gem "standard"
9
- gem "vcr"
10
- gem "webmock"
11
- gem "braintree", ">= 2.92.0"
12
- gem "lemonsqueezy", "~> 1.0"
13
- gem "paddle", "~> 2.6"
14
- gem "stripe", "~> 13.0"
15
- gem "prawn"
16
- gem "receipts"
17
- gem "sqlite3"
18
- gem "pg"
19
- gem "bootsnap", require: false
20
- gem "puma"
21
- gem "web-console", group: :development
22
- gem "importmap-rails"
23
- gem "sprockets-rails"
24
- gem "stimulus-rails"
25
- gem "turbo-rails"
26
- gem "rdoc", ">= 7.0"
5
+ gem "rake", "~> 13.0"
27
6
  gem "pay", "~> 8.3.0"
7
+ gem "stripe", "~> 13.0"
8
+ gem "rails", "~> 8.1.0"
9
+
10
+ group :development do
11
+ gem "appraisal"
12
+ gem "web-console"
13
+ gem "standard"
14
+ gem "rubocop", "~> 1.0"
15
+ gem "rubocop-minitest", "~> 0.35"
16
+ gem "rubocop-performance", "~> 1.0"
17
+ end
18
+
19
+ group :test do
20
+ gem "minitest", "~> 5.0"
21
+ gem "mocha"
22
+ gem "simplecov", require: false
23
+ gem "vcr"
24
+ gem "webmock"
25
+ gem "braintree", ">= 2.92.0"
26
+ gem "lemonsqueezy", "~> 1.0"
27
+ gem "paddle", "~> 2.6"
28
+ gem "prawn"
29
+ gem "receipts"
30
+ gem "sqlite3"
31
+ gem "pg"
32
+ gem "bootsnap", require: false
33
+ gem "puma"
34
+ gem "importmap-rails"
35
+ gem "sprockets-rails"
36
+ gem "stimulus-rails"
37
+ gem "turbo-rails"
38
+ gem "rdoc", ">= 7.0"
39
+ end
28
40
 
29
41
  gemspec path: "../"
@@ -2,28 +2,40 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal"
6
- gem "mocha"
7
- gem "simplecov", require: false
8
- gem "standard"
9
- gem "vcr"
10
- gem "webmock"
11
- gem "braintree", ">= 2.92.0"
12
- gem "lemonsqueezy", "~> 1.0"
13
- gem "paddle", "~> 2.6"
14
- gem "stripe", "~> 13.0"
15
- gem "prawn"
16
- gem "receipts"
17
- gem "sqlite3"
18
- gem "pg"
19
- gem "bootsnap", require: false
20
- gem "puma"
21
- gem "web-console", group: :development
22
- gem "importmap-rails"
23
- gem "sprockets-rails"
24
- gem "stimulus-rails"
25
- gem "turbo-rails"
26
- gem "rdoc", ">= 7.0"
5
+ gem "rake", "~> 13.0"
27
6
  gem "pay", "~> 9.0.0"
7
+ gem "stripe", "~> 13.0"
8
+ gem "rails", "~> 8.1.0"
9
+
10
+ group :development do
11
+ gem "appraisal"
12
+ gem "web-console"
13
+ gem "standard"
14
+ gem "rubocop", "~> 1.0"
15
+ gem "rubocop-minitest", "~> 0.35"
16
+ gem "rubocop-performance", "~> 1.0"
17
+ end
18
+
19
+ group :test do
20
+ gem "minitest", "~> 5.0"
21
+ gem "mocha"
22
+ gem "simplecov", require: false
23
+ gem "vcr"
24
+ gem "webmock"
25
+ gem "braintree", ">= 2.92.0"
26
+ gem "lemonsqueezy", "~> 1.0"
27
+ gem "paddle", "~> 2.6"
28
+ gem "prawn"
29
+ gem "receipts"
30
+ gem "sqlite3"
31
+ gem "pg"
32
+ gem "bootsnap", require: false
33
+ gem "puma"
34
+ gem "importmap-rails"
35
+ gem "sprockets-rails"
36
+ gem "stimulus-rails"
37
+ gem "turbo-rails"
38
+ gem "rdoc", ">= 7.0"
39
+ end
28
40
 
29
41
  gemspec path: "../"
@@ -0,0 +1,41 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rails", "~> 7.2.0"
7
+ gem "pay", "~> 11.0"
8
+ gem "stripe", "~> 18.0"
9
+
10
+ group :development do
11
+ gem "appraisal"
12
+ gem "web-console"
13
+ gem "standard"
14
+ gem "rubocop", "~> 1.0"
15
+ gem "rubocop-minitest", "~> 0.35"
16
+ gem "rubocop-performance", "~> 1.0"
17
+ end
18
+
19
+ group :test do
20
+ gem "minitest", "~> 5.0"
21
+ gem "mocha"
22
+ gem "simplecov", require: false
23
+ gem "vcr"
24
+ gem "webmock"
25
+ gem "braintree", ">= 2.92.0"
26
+ gem "lemonsqueezy", "~> 1.0"
27
+ gem "paddle", "~> 2.6"
28
+ gem "prawn"
29
+ gem "receipts"
30
+ gem "sqlite3"
31
+ gem "pg"
32
+ gem "bootsnap", require: false
33
+ gem "puma"
34
+ gem "importmap-rails"
35
+ gem "sprockets-rails"
36
+ gem "stimulus-rails"
37
+ gem "turbo-rails"
38
+ gem "rdoc", ">= 7.0"
39
+ end
40
+
41
+ gemspec path: "../"
@@ -0,0 +1,41 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rails", "~> 8.1.0"
7
+ gem "pay", "~> 11.0"
8
+ gem "stripe", "~> 18.0"
9
+
10
+ group :development do
11
+ gem "appraisal"
12
+ gem "web-console"
13
+ gem "standard"
14
+ gem "rubocop", "~> 1.0"
15
+ gem "rubocop-minitest", "~> 0.35"
16
+ gem "rubocop-performance", "~> 1.0"
17
+ end
18
+
19
+ group :test do
20
+ gem "minitest", "~> 5.0"
21
+ gem "mocha"
22
+ gem "simplecov", require: false
23
+ gem "vcr"
24
+ gem "webmock"
25
+ gem "braintree", ">= 2.92.0"
26
+ gem "lemonsqueezy", "~> 1.0"
27
+ gem "paddle", "~> 2.6"
28
+ gem "prawn"
29
+ gem "receipts"
30
+ gem "sqlite3"
31
+ gem "pg"
32
+ gem "bootsnap", require: false
33
+ gem "puma"
34
+ gem "importmap-rails"
35
+ gem "sprockets-rails"
36
+ gem "stimulus-rails"
37
+ gem "turbo-rails"
38
+ gem "rdoc", ">= 7.0"
39
+ end
40
+
41
+ gemspec path: "../"
@@ -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
- # Alert when balance drops below this threshold (default: 100 credits)
87
- # Set to nil to disable low balance alerts
88
- #
89
- # config.low_balance_threshold = 100.credits
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
- # Handle low credit balance alerts Useful to sell booster credit packs, for example
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
- # config.on_low_balance do |user|
95
- # Send notification to user when their balance drops below the threshold
96
- # UserMailer.low_credits_alert(user).deliver_later
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