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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +1 -1
- data/lib/usage_credits/models/concerns/pay_subscription_extension.rb +39 -5
- data/lib/usage_credits/version.rb +1 -1
- metadata +16 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5aa140a73174bbd4c7499e93674a24e2ec246251e0e70e60c8fa1ec95b829b8
|
|
4
|
+
data.tar.gz: 1999d308a326acfd2ad5a4b1eac87b61f7322af8ee21b2a1c2cca6b4f0479e0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
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
|
-
|
|
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
|
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.
|
|
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-
|
|
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: '
|
|
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: '
|
|
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
|