usage_credits 1.1.1 → 1.2.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: 10c8b86a5ff03a6d32bc494739245bbf3a829966620931efb6fcea8c72d327bf
4
- data.tar.gz: deec62e5e448b924dec3e2896491c3723dbb07f7b90f2945b13d117b64a1c444
3
+ metadata.gz: b5aa140a73174bbd4c7499e93674a24e2ec246251e0e70e60c8fa1ec95b829b8
4
+ data.tar.gz: 1999d308a326acfd2ad5a4b1eac87b61f7322af8ee21b2a1c2cca6b4f0479e0f
5
5
  SHA512:
6
- metadata.gz: acef2a23c57df23db5738fbee7d718f15a0883404cd059f6e04e049e2fa00777ab37894b9ccc4abae11bdbe1cf0c5a4374dd682d80ef33ccac44f560399d6e8d
7
- data.tar.gz: aee4071cbae93898c31014bac55fc08f2b196f9d4831ab64f749b3a4802d97499dd9a9f73e5cd8cc027bf154a2c6283c62b9d96ce60ebd0543d7ba18435c3ef4
6
+ metadata.gz: eb02af549932498a5babbd50a6c527db28280ac4f518fb687e94f204fead15c30ae3a1f72644e019e3ec7888262a5194f4760e49013bf0d6b2bf6d01e6368910
7
+ data.tar.gz: e2c3f6f7a04f8cbe84a61f9577b46a56589d27e41c3c87f9918643124aff85e0fe29cd61efe5959212341e2691c6846110e6e5d32a5fad4ae640d6626a046e64
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## [1.2.0] - 2026-10-26
2
+
3
+ ### Added
4
+
5
+ - **Pay 12 support.** The `pay` dependency is now `>= 11.6.2, < 13.0`, except 12.0.0 and 12.1.0. Those two releases accept forged Paddle Billing and Lemon Squeezy webhooks when no signing secret is set ([GHSA-5p37-w3f3-55pm](https://github.com/pay-rails/pay/security/advisories/GHSA-5p37-w3f3-55pm), [GHSA-6mr3-672j-v5hf](https://github.com/pay-rails/pay/security/advisories/GHSA-6mr3-672j-v5hf)), and a forged subscription webhook would mint plan credits. Pay 12.1.1 fixes both. Pay checks processor gem versions at boot: Pay 12.3 needs `stripe ~> 20` (from `~> 19`) and, if you use Paddle, `paddle ~> 3` (from `~> 2.5`), which requires Ruby 3.3+. Pay 12.1.1 to 12.2 keep Pay 11's `stripe` and `paddle` versions. Pay 12 ships no new migrations, so there is no schema change. A scheduled Paddle Billing pause and Stripe's `paused` status, which Pay 12.2 reads differently, need no change: the first keeps awarding credits until the pause starts, and the second awards none. Based on #47 by @krzyzak.
6
+
7
+ ### Changed
8
+
9
+ - **Lemon Squeezy cancellations take effect when the subscription ends, on Pay 11 and Pay 12.** Lemon Squeezy records `cancelled` (Pay 12 stores `canceled`) at the cancel request, while the customer has paid until `ends_at` and can still resume. It records `expired` when the subscription ends, and also when a failed dunning ends (`past_due`, `unpaid`, `expired`). The cancellation handling now runs on `expired`, or on a cancellation whose `ends_at` has passed: the fulfillment stops at `ends_at`, and plans with `expire_after` expire the subscription's credits. Stripe, Paddle and Braintree are unchanged: they record `canceled` only at the end. Before, this handling never ran for Lemon Squeezy. Pay 11 stores `cancelled`, and the check matched only `canceled`. `stops_at` already followed the billing period, so only `expire_after` was skipped. On Pay 12, the check would have matched at the cancel request and expired credits that a resume cannot restore.
10
+ - **A cancelled Lemon Squeezy subscription keeps its recurring credits until `ends_at`**, as Stripe and Paddle subscriptions do while a cancellation at the end of the period is pending. Before, credits on a shorter cadence than billing (for example daily credits on a monthly plan) stopped at the cancel request. A cancel request never starts an initial award. When you deploy this release, a Lemon Squeezy subscription in its cancellation grace period with an overdue fulfillment gets one catch-up period at the next job run, as a Stripe subscription does.
11
+ - **A Lemon Squeezy pause depends on the status only.** Pay 11 stored the pause's resume time in `pause_starts_at`, and Pay 12 never clears that column. Without this change, a Lemon Squeezy subscription that was paused when you moved to Pay 12 got no more credits after it resumed.
12
+
13
+ ### Upgrading to Pay 12
14
+
15
+ - **Lemon Squeezy rows.** Pay's [12.0 upgrade guide](https://github.com/pay-rails/pay/blob/main/UPGRADE.md#lemon-squeezy) gives two ways to rewrite the rows that Pay 11 synced. With either way, also run its line that moves the old pause value: `Pay::LemonSqueezy::Subscription.where.not(pause_starts_at: nil).find_each { |s| s.update_columns(pause_resumes_at: s.pause_starts_at, pause_starts_at: nil) }`.
16
+ - `find_each(&:sync!)` reads each subscription from Lemon Squeezy again and saves it with callbacks. A row that Lemon Squeezy now reports as `expired` moves to `expired` and runs the cancellation handling. On plans with `expire_after`, its credits expire at `ends_at` plus the `expire_after` duration, which has already passed for most of these rows. A row that is still `cancelled` becomes `canceled` and keeps its credits until Lemon Squeezy sends `expired`. Rows that Pay 11 already stored as `expired` get no handling.
17
+ - The `update_all` variant skips callbacks. It changes no fulfillment or credit, and the handling runs when Lemon Squeezy next sends `expired`.
18
+ - **Checkout helpers.** `CreditPack#create_checkout_session` and `CreditSubscriptionPlan#create_checkout_session` call Pay's `checkout`. On Pay 12, a Stripe failure there raises `Pay::Stripe::Error`, not `Stripe::StripeError`, so rescue `Pay::Error` around the helpers (the Stripe error is the `cause`). Stripe 20 (Pay 12.3) no longer takes `payment_method_types`, so pass `allowed_payment_method_types` to `CreditPack#create_checkout_session` instead.
19
+
20
+ ### Tests
21
+
22
+ - CI runs the suite against Pay 12.3 (`rails-7.2` and `rails-8.1`, now pinned to `pay ~> 12.3`), Pay 12.2 (new `pay-12-stripe-19` appraisal: Pay 12.1.1 to 12.2, `stripe ~> 19`, `paddle ~> 2.6`, Rails 8.1), the latest Pay 11 (new `pay-11` appraisal, Rails 8.1) and the Pay 11.6.2 floor. Pay 12.3's `paddle ~> 3` needs Ruby 3.3+, so Ruby 3.2 runs `pay-12-stripe-19`, `pay-11` and `pay-minimum`. `pay-minimum` now pins Rails 7.2 and is the literal Ruby 3.2 / Rails 7.2 / Pay 11.6.2 minimum that the PostgreSQL and MySQL minimum jobs run.
23
+ - The Lemon Squeezy tests go through Pay's own `sync`, so each Pay line maps the statuses itself: a trial, a cancel request and a resume, a cancellation that ends in `expired`, a failed dunning, recurring credits before `ends_at`, and a pause that Pay 11 wrote. The Paddle Billing scheduled pause and Stripe `paused` tests check Pay's `paused?` per Pay version.
24
+ - The development Gemfile adds `ostruct`: braintree requires it without declaring it, Ruby 4.0 no longer ships it as a default gem, and `paddle` 3 no longer pulls it in.
25
+
1
26
  ## [1.1.1] - 2026-10-19
2
27
 
3
28
  ### Fixed
data/README.md CHANGED
@@ -21,7 +21,7 @@ All with a simple DSL that reads just like English.
21
21
 
22
22
  - Ruby 3.2+ and Rails 7.2.3.1–8.x. Ruby 3.2 is the security floor because current patched versions of transitive Rails dependencies no longer support Ruby 3.1.
23
23
  - An ActiveJob backend (Sidekiq, `solid_queue`, etc.) for subscription credit fulfillment
24
- - [`pay`](https://github.com/pay-rails/pay) 11.6.2–11.x for payment integration. Pay 11.6.2 is the security floor because it fixes [GHSA-mjgf-xj26-9qf9](https://github.com/pay-rails/pay/security/advisories/GHSA-mjgf-xj26-9qf9).
24
+ - [`pay`](https://github.com/pay-rails/pay) 11.6.2–11.x or 12.1.1–12.x for payment integration. Pay 11.6.2 is the security floor because it fixes [GHSA-mjgf-xj26-9qf9](https://github.com/pay-rails/pay/security/advisories/GHSA-mjgf-xj26-9qf9). Pay 12.0.0 and 12.1.0 are excluded because they accept forged Paddle Billing and Lemon Squeezy webhooks when no signing secret is set ([GHSA-5p37-w3f3-55pm](https://github.com/pay-rails/pay/security/advisories/GHSA-5p37-w3f3-55pm), [GHSA-6mr3-672j-v5hf](https://github.com/pay-rails/pay/security/advisories/GHSA-6mr3-672j-v5hf)). Pay 12.3 requires `stripe ~> 20` and, for Paddle, `paddle ~> 3` (Ruby 3.3+); Pay 11 and Pay 12.1.1–12.2 require `stripe ~> 19` and `paddle ~> 2.5`. If you use Lemon Squeezy and move to Pay 12, run Pay's one-time row update together with its line that moves `pause_starts_at` to `pause_resumes_at`, and read the 1.2.0 upgrade notes in the [CHANGELOG](CHANGELOG.md) first.
25
25
  - [`wallets`](https://github.com/rameerez/wallets) 0.4.2 or any later 0.4.x (installed automatically as the ledger core)
26
26
 
27
27
  ## 👨‍💻 Example
@@ -28,6 +28,9 @@ module UsageCredits
28
28
  module PaySubscriptionExtension
29
29
  extend ActiveSupport::Concern
30
30
 
31
+ # Lemon Squeezy's own spelling, which Pay 11 stores, and Pay 12's spelling.
32
+ LEMON_SQUEEZY_CANCELED_STATUSES = %w[cancelled canceled].freeze
33
+
31
34
  included do
32
35
  # For initial setup and fulfillment, we can't do after_create or on: :create because the subscription first may
33
36
  # get created with status "incomplete" and only get updated to status "active" when the payment is cleared
@@ -64,6 +67,10 @@ module UsageCredits
64
67
  def eligible_for_usage_credit_fulfillment?(include_trial: false)
65
68
  processor_active = if status == "on_trial"
66
69
  respond_to?(:on_trial?) && on_trial?
70
+ elsif lemon_squeezy_cancellation_grace_period?
71
+ # The customer paid until ends_at and can still resume, as with a
72
+ # Stripe or Paddle cancellation at the end of the period.
73
+ true
67
74
  elsif respond_to?(:paused?)
68
75
  active?
69
76
  else
@@ -155,12 +162,31 @@ module UsageCredits
155
162
  eligible_for_usage_credit_fulfillment?
156
163
  end
157
164
 
158
- # This doesn't get called the exact moment the user cancels its subscription, but at the end of the period,
159
- # when the payment processor sends the event that the subscription has actually been cancelled.
160
- # For the moment the user clicks on "Cancel subscription", the sub keeps its state as "active" (for now),
161
- # the sub just gets its `ends_at` set from nil to the actual cancellation date.
165
+ # True when the subscription has really ended, not when the customer asks
166
+ # to cancel. Stripe, Paddle and Braintree keep the status "active" (with
167
+ # `ends_at` set) until the end of the paid period, and only then record
168
+ # "canceled". Lemon Squeezy records "cancelled" (Pay 12: "canceled") at
169
+ # the cancel request, while the customer can still resume. Its terminal
170
+ # status is "expired", which also ends a failed dunning
171
+ # (past_due, unpaid, expired).
162
172
  def subscription_canceled?
163
- saved_change_to_status? && status == "canceled"
173
+ return false unless saved_change_to_status?
174
+ return status == "canceled" unless lemon_squeezy_subscription?
175
+
176
+ status == "expired" ||
177
+ (LEMON_SQUEEZY_CANCELED_STATUSES.include?(status) && !lemon_squeezy_cancellation_grace_period?)
178
+ end
179
+
180
+ def lemon_squeezy_subscription?
181
+ is_a?(Pay::LemonSqueezy::Subscription)
182
+ end
183
+
184
+ # A Lemon Squeezy subscription that the customer cancelled, inside the
185
+ # period they paid for.
186
+ def lemon_squeezy_cancellation_grace_period?
187
+ lemon_squeezy_subscription? &&
188
+ LEMON_SQUEEZY_CANCELED_STATUSES.include?(status) &&
189
+ ends_at.present? && ends_at > Time.current
164
190
  end
165
191
 
166
192
  def plan_changed?
@@ -191,6 +217,10 @@ module UsageCredits
191
217
 
192
218
  # Immediate awarding of first cycle + set up Fulfillment object for subsequent periods
193
219
  def handle_initial_award_and_fulfillment_setup
220
+ # A cancel request never starts an award. The paid period's credits
221
+ # were granted already, and the recurring fulfillment runs to ends_at.
222
+ return if lemon_squeezy_cancellation_grace_period?
223
+
194
224
  plan = subscription_terms
195
225
  return unless plan
196
226
  return unless has_valid_wallet?
@@ -869,6 +899,10 @@ module UsageCredits
869
899
 
870
900
  def paused_for_usage_credits?
871
901
  return true if status == "paused"
902
+ # A Lemon Squeezy pause starts when Lemon Squeezy records it, so the
903
+ # status is enough. Pay 11 kept the pause's resume time in
904
+ # pause_starts_at, and Pay 12 never clears that old value.
905
+ return false if lemon_squeezy_subscription?
872
906
 
873
907
  pause_started = respond_to?(:pause_starts_at) && pause_starts_at.present? && pause_starts_at <= Time.current
874
908
  return true if pause_started
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UsageCredits
4
- VERSION = "1.1.1"
4
+ VERSION = "1.2.0"
5
5
  end
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: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-18 00:00:00.000000000 Z
10
+ date: 2026-10-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pay
@@ -18,7 +18,13 @@ dependencies:
18
18
  version: 11.6.2
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: '12.0'
21
+ version: '13.0'
22
+ - - "!="
23
+ - !ruby/object:Gem::Version
24
+ version: 12.0.0
25
+ - - "!="
26
+ - !ruby/object:Gem::Version
27
+ version: 12.1.0
22
28
  type: :runtime
23
29
  prerelease: false
24
30
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,7 +34,13 @@ dependencies:
28
34
  version: 11.6.2
29
35
  - - "<"
30
36
  - !ruby/object:Gem::Version
31
- version: '12.0'
37
+ version: '13.0'
38
+ - - "!="
39
+ - !ruby/object:Gem::Version
40
+ version: 12.0.0
41
+ - - "!="
42
+ - !ruby/object:Gem::Version
43
+ version: 12.1.0
32
44
  - !ruby/object:Gem::Dependency
33
45
  name: rails
34
46
  requirement: !ruby/object:Gem::Requirement