pricing_plans 0.1.1 → 0.2.1
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/.claude/settings.local.json +11 -8
- data/Appraisals +9 -0
- data/CHANGELOG.md +7 -4
- data/README.md +20 -4
- data/docs/01-define-pricing-plans.md +66 -9
- data/docs/04-views.md +2 -1
- data/docs/06-gem-compatibility.md +31 -4
- data/gemfiles/rails_7.2.gemfile +23 -0
- data/gemfiles/rails_8.1.gemfile +23 -0
- data/lib/generators/pricing_plans/install/templates/initializer.rb +4 -0
- data/lib/pricing_plans/configuration.rb +13 -5
- data/lib/pricing_plans/controller_guards.rb +3 -2
- data/lib/pricing_plans/limit_checker.rb +5 -2
- data/lib/pricing_plans/limitable.rb +9 -2
- data/lib/pricing_plans/pay_support.rb +62 -18
- data/lib/pricing_plans/plan.rb +14 -1
- data/lib/pricing_plans/plan_resolver.rb +49 -6
- data/lib/pricing_plans/version.rb +1 -1
- data/lib/pricing_plans.rb +9 -2
- metadata +5 -100
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 231aa688b5fb69cffde68df9bbab35af32634d8605930559378e15a841cb6aa0
|
|
4
|
+
data.tar.gz: 2fee172963111d141b39d9ad9335f1b84d3f9b6f46df139e63684103eecb7750
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99277aae15c2a137b0824a199097420a60613e9c5c20b6569036f11b574186997b303fdfe42d5a2d147a66d0b28a32812e8ca1706903d94c88f6e61a5e09bf27
|
|
7
|
+
data.tar.gz: 12aeee03c7911d19b7b4c04398946150534b0ef66d59aa50211992a52f3000908778e42e7eb51b2acc0bec85e23e51eff5996ae04870f948826485ebf80a1414
|
data/.claude/settings.local.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(mkdir:*)",
|
|
5
|
-
"Bash(bundle install:*)",
|
|
6
|
-
"Bash(bundle exec rake:*)",
|
|
7
|
-
"Bash(bundle update:*)",
|
|
8
|
-
"Bash(bundle exec ruby:*)",
|
|
9
4
|
"Bash(sed:*)",
|
|
10
5
|
"Bash(grep:*)",
|
|
11
|
-
"Bash(
|
|
12
|
-
"
|
|
6
|
+
"Bash(find:*)",
|
|
7
|
+
"mcp__context7__resolve-library-id",
|
|
8
|
+
"mcp__context7__get-library-docs",
|
|
9
|
+
"WebFetch(domain:github.com)",
|
|
10
|
+
"WebSearch",
|
|
11
|
+
"Bash(bundle exec rake test:*)",
|
|
12
|
+
"Bash(bundle install:*)",
|
|
13
|
+
"Bash(bundle exec appraisal:*)",
|
|
14
|
+
"Bash(ls:*)",
|
|
15
|
+
"Bash(done)"
|
|
13
16
|
],
|
|
14
17
|
"deny": []
|
|
15
18
|
}
|
|
16
|
-
}
|
|
19
|
+
}
|
data/Appraisals
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
## [0.2.1] - 2026-01-15
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- Added a `metadata` alias to plans, and documented its usage
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## [0.2.0] - 2025-12-26
|
|
6
|
+
|
|
7
|
+
- Fix a bug in the pay gem integration that would always return the default pricing plan regardless of the actual Pay subscription
|
|
8
|
+
- Add hidden plans, enabling grandfathering, no-free-users use cases, etc.
|
|
9
|
+
- Prevent unlimited limits for limits that were undefined
|
|
7
10
|
|
|
8
11
|
## [0.1.1] - 2025-12-25
|
|
9
12
|
|
data/README.md
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# 💵 `pricing_plans` - Define and enforce pricing plan limits in your Rails app (SaaS entitlements)
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/pricing_plans) [](https://github.com/rameerez/pricing_plans/actions)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
`pricing_plans` allows you to enforce pricing plan limits with one-liners that read like plain English. Avoid scattering and entangling pricing logic everywhere in your Rails SaaS.
|
|
6
9
|
|
|
7
10
|
For example, this is how you define pricing plans and their entitlements:
|
|
8
11
|
```ruby
|
|
9
12
|
plan :pro do
|
|
10
|
-
allows :api_access
|
|
11
|
-
limits :projects, to: 5
|
|
13
|
+
allows :api_access # Features: blocked by default unless explicitly allowed
|
|
14
|
+
limits :projects, to: 5 # Limits: 0 by default unless a limit is set explicitly
|
|
12
15
|
end
|
|
13
16
|
```
|
|
14
17
|
|
|
18
|
+
Plans are **secure by default**: features are disabled and limits are set to 0 unless explicitly configured.
|
|
19
|
+
|
|
15
20
|
You can then gate features in your controllers:
|
|
16
21
|
```ruby
|
|
17
22
|
before_action :enforce_api_access!, only: [:create]
|
|
@@ -97,6 +102,17 @@ You can also display upgrade alerts to prompt users into upgrading to the next p
|
|
|
97
102
|
|
|
98
103
|

|
|
99
104
|
|
|
105
|
+
You can attach arbitrary plan `metadata` for UI/presentation needs (icons, colors, badges) directly in the initializer:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
plan :hobby do
|
|
109
|
+
metadata icon: "rocket", color: "bg-red-500"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
plan.metadata[:icon] # => "rocket"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also grandfather users into old plans (hidden to other users), assign plans manually without requiring a payment (for testing, gifts, or employees), and much more!
|
|
100
116
|
|
|
101
117
|
## 🤓 Read the docs!
|
|
102
118
|
|
|
@@ -20,7 +20,9 @@ That's the basics! Let's dive in.
|
|
|
20
20
|
|
|
21
21
|
## Define what each plan gives
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Plans are **secure by default**: features are disabled and limits are set to 0 unless explicitly configured.
|
|
24
|
+
|
|
25
|
+
At a high level, a plan does **two** things:
|
|
24
26
|
(1) Gate features
|
|
25
27
|
(2) Enforce limits (quotas)
|
|
26
28
|
|
|
@@ -41,7 +43,7 @@ end
|
|
|
41
43
|
We're just **defining** what the plan does now. Later, we'll see [all the methods we can use to enforce these limits and gate these features](#gate-features-in-controllers) very easily.
|
|
42
44
|
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
**Features and limits are secure by default**: features are disabled and limits are set to 0 unless explicitly allowed. For clarity, you can explicitly state what a plan disallows, but this is just cosmetic:
|
|
45
47
|
|
|
46
48
|
```ruby
|
|
47
49
|
PricingPlans.configure do |config|
|
|
@@ -252,16 +254,15 @@ end
|
|
|
252
254
|
|
|
253
255
|
(Assuming, of course, that your `Project` model has an `active` scope)
|
|
254
256
|
|
|
255
|
-
|
|
257
|
+
**Undefined limits default to 0 (blocked).** To explicitly allow unlimited access, use the `unlimited` helper:
|
|
256
258
|
|
|
257
259
|
```ruby
|
|
258
260
|
PricingPlans.configure do |config|
|
|
259
|
-
plan :
|
|
260
|
-
price
|
|
261
|
-
|
|
261
|
+
plan :enterprise do
|
|
262
|
+
price 999
|
|
262
263
|
allows :api_access
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
unlimited :projects # Explicit unlimited
|
|
265
|
+
# :storage undefined → 0 (blocked)
|
|
265
266
|
end
|
|
266
267
|
end
|
|
267
268
|
```
|
|
@@ -299,6 +300,7 @@ PricingPlans.configure do |config|
|
|
|
299
300
|
name "Free Plan" # optional, would default to "Free" as inferred from the :free key
|
|
300
301
|
description "A plan to get you started"
|
|
301
302
|
bullets "Basic features", "Community support"
|
|
303
|
+
metadata icon: "rocket", color: "bg-red-500"
|
|
302
304
|
|
|
303
305
|
cta_text "Subscribe"
|
|
304
306
|
# In initializers, prefer a string path/URL or set a global default CTA in config.
|
|
@@ -314,6 +316,61 @@ end
|
|
|
314
316
|
|
|
315
317
|
You can also make a plan `default!`; and you can make a plan `highlighted!` to help you when building a pricing table.
|
|
316
318
|
|
|
319
|
+
### Plan metadata for UI and presentation
|
|
320
|
+
|
|
321
|
+
You can attach arbitrary `metadata` to a plan for presentation needs (for example, per-card icons or colors on a pricing page). This keeps plan UI details co-located in the same DSL rather than scattered elsewhere:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
plan :hobby do
|
|
325
|
+
metadata icon: "rocket", color: "bg-red-500"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
plan.metadata[:icon] # => "rocket"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Hide plans from public lists
|
|
332
|
+
|
|
333
|
+
You can mark a plan as `hidden!` to exclude it from public-facing plan lists (`PricingPlans.plans`, `PricingPlans.for_pricing`, `PricingPlans.view_models`). Hidden plans are still accessible internally and can be assigned to users.
|
|
334
|
+
|
|
335
|
+
**Use cases for hidden plans:**
|
|
336
|
+
- **Default plan for unsubscribed users**: Create a `hidden!` plan with zero limits as your default for users who haven't subscribed yet. This is useful, for example, if you don't want free users in your app (everyone needs to pay) -- the `hidden!` plan would get assigned to every user by default (to implicitly block access to all features) until they subscribe
|
|
337
|
+
- **Grandfathered plans**: Old plans you no longer offer to new customers, but existing users still have
|
|
338
|
+
- **Internal/testing plans**: Plans for employees, beta testers, or special partnerships
|
|
339
|
+
- **Deprecated plans**: Plans being phased out but still active for some users
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
PricingPlans.configure do |config|
|
|
343
|
+
# Hidden default plan for users who haven't subscribed
|
|
344
|
+
# It won't appear on pricing page
|
|
345
|
+
# This is what users are on before they subscribe to any plan
|
|
346
|
+
plan :unsubscribed do
|
|
347
|
+
price 0
|
|
348
|
+
hidden! # Won't appear on pricing page
|
|
349
|
+
default!
|
|
350
|
+
# No limits defined - everything defaults to 0 (blocked)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Visible plans for your pricing page
|
|
354
|
+
plan :starter do
|
|
355
|
+
price 10
|
|
356
|
+
limit :projects, to: 5
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Grandfathered plan (hidden from new customers)
|
|
360
|
+
plan :legacy_2020 do
|
|
361
|
+
price 15
|
|
362
|
+
hidden! # Existing customers keep it, but won't show on pricing page
|
|
363
|
+
limit :projects, to: 100
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Important notes:**
|
|
369
|
+
- Hidden plans can be the `default!` plan (common pattern for "unsubscribed" users)
|
|
370
|
+
- Hidden plans **cannot** be the `highlighted!` plan (validation error - highlighted plans must be visible)
|
|
371
|
+
- Users can still be on hidden plans (via Pay subscription, manual assignment, or default)
|
|
372
|
+
- Internal APIs (`Registry.plans`, `PlanResolver`) can still access hidden plans
|
|
373
|
+
- Pay gem can still resolve subscriptions to hidden plans (useful for grandfathered customers)
|
|
317
374
|
|
|
318
375
|
## Link paid plans to Stripe prices (requires `pay`)
|
|
319
376
|
|
|
@@ -369,4 +426,4 @@ plan :enterprise do
|
|
|
369
426
|
unlimited :products
|
|
370
427
|
allows :api_access, :premium_features
|
|
371
428
|
end
|
|
372
|
-
```
|
|
429
|
+
```
|
data/docs/04-views.md
CHANGED
|
@@ -18,6 +18,7 @@ Each `PricingPlans::Plan` responds to:
|
|
|
18
18
|
- `plan.price_label` → The `price` or `price_string` you've defined for the plan. If `stripe_price` is set and the Stripe gem is available, it auto-fetches the live price from Stripe. You can override or disable this.
|
|
19
19
|
- `plan.cta_text`
|
|
20
20
|
- `plan.cta_url`
|
|
21
|
+
- `plan.metadata` → Optional hash for UI/presentation attributes (icons, colors, badges)
|
|
21
22
|
|
|
22
23
|
### Example: build a pricing page
|
|
23
24
|
|
|
@@ -118,4 +119,4 @@ Tip: you could also use `plan_limit_remaining(:projects)` and `plan_limit_percen
|
|
|
118
119
|
|
|
119
120
|
## Message customization
|
|
120
121
|
|
|
121
|
-
- You can override copy globally via `config.message_builder` in [`pricing_plans.rb`](/docs/01-define-pricing-plans.md), which is used across limit checks and features. Suggested signature: `(context:, **kwargs) -> string` with contexts `:over_limit`, `:grace`, `:feature_denied`, and `:overage_report`.
|
|
122
|
+
- You can override copy globally via `config.message_builder` in [`pricing_plans.rb`](/docs/01-define-pricing-plans.md), which is used across limit checks and features. Suggested signature: `(context:, **kwargs) -> string` with contexts `:over_limit`, `:grace`, `:feature_denied`, and `:overage_report`.
|
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
# Using `pricing_plans` with `pay` and/or `usage_credits`
|
|
2
2
|
|
|
3
|
-
`pricing_plans` is designed to work seamlessly with other complementary popular gems like `pay` (to handle actual subscriptions and payments), and `usage_credits` (to handle credit-like spending and refills)
|
|
3
|
+
`pricing_plans` is designed to work seamlessly with other complementary popular gems like [`pay`](https://github.com/pay-rails/pay) (to handle actual subscriptions and payments), and `usage_credits` (to handle credit-like spending and refills)
|
|
4
4
|
|
|
5
|
-
These gems are related but not overlapping. They're complementary. The boundaries are
|
|
5
|
+
These gems are related but not overlapping. They're complementary. The boundaries are:
|
|
6
|
+
- [`pay`](https://github.com/pay-rails/pay) handles billing
|
|
7
|
+
- [`usage_credits`](https://github.com/rameerez/usage_credits/) handles user credits (metered usage through credits, ledger-like)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## `pay` gem
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
The integration with the `pay` gem should be seamless and is documented throughout the entire docs; however, to make it explicit:
|
|
12
|
+
|
|
13
|
+
There's nothing to do on your end to make `pricing_plans` work with `pay`!
|
|
14
|
+
|
|
15
|
+
As long as your `pricing_plans` config (`config/initializers/pricing_plans.rb`) contains a plan with the correct `stripe_price` ID, whenever a subscription to that Stripe price ID is found through the `pay` gem, `pricing_plans` will understand the user is subscribed to that plan automatically, and will start enforcing the corresponding limits.
|
|
16
|
+
|
|
17
|
+
The way `pricing_plans` works doesn't require any data migration, or callback setup, or any manual action. You don't need to call `assign_pricing_plan!` at all at any point, unless you're trying to something like overriding a plan, gifting users access to plans without any payment, or things like that.
|
|
18
|
+
|
|
19
|
+
As long as a matching `stripe_price` is found in the `pricing_plans.rb` initializer, the gem will know a user subscribed to that Stripe price ID is under the corresponding plan. Essentially, the gem just looks at the current `pay` subscriptions of your user. If a matching price ID is found in the `pricing_plans` configuration file, it enforces the corresponding limits.
|
|
20
|
+
|
|
21
|
+
> [!TIP]
|
|
22
|
+
> To make your `pricing_plans` gem config work across environments (production, development, etc.) instead of defining price IDs statically like this in the config:
|
|
23
|
+
>
|
|
24
|
+
> ```ruby
|
|
25
|
+
> stripe_price month: "price_123", year: "price_456"
|
|
26
|
+
> ```
|
|
27
|
+
>
|
|
28
|
+
> Try instead defining them dynamically using `Rails.env`, so the corresponding plan for each environment gets loaded automatically. A simple solution would be to define your plans in the credentials file, and then doing something like this in the `pricing_plans` config:
|
|
29
|
+
>
|
|
30
|
+
> ```ruby
|
|
31
|
+
> stripe_price month: Rails.application.credentials.dig(Rails.env.to_sym, :stripe_plans, :plan_name, :monthly), year: Rails.application.credentials.dig(Rails.env.to_sym, :stripe_plans, :plan_name, :yearly)
|
|
32
|
+
> ```
|
|
33
|
+
>
|
|
34
|
+
> You can come up with similar solutions, like adding that config to a plaintext `.yml` file if you don't want to store this info in the credentials file, but this is the overall idea.
|
|
35
|
+
|
|
36
|
+
## `usage_credits` gem
|
|
10
37
|
|
|
11
38
|
In the SaaS world, pricing plans and usage credits are related in so far credits are usually a part of a pricing plan. A plan would give you, say, 100 credits a month along other features, and users would find that information usually documented in the pricing table itself.
|
|
12
39
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 7.2.3"
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "irb"
|
|
11
|
+
gem "rubocop", "~> 1.0"
|
|
12
|
+
gem "rubocop-minitest", "~> 0.35"
|
|
13
|
+
gem "rubocop-performance", "~> 1.0"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
group :test do
|
|
17
|
+
gem "minitest", "~> 5.0"
|
|
18
|
+
gem "sqlite3", "~> 2.1"
|
|
19
|
+
gem "ostruct"
|
|
20
|
+
gem "simplecov", require: false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.1.2"
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "irb"
|
|
11
|
+
gem "rubocop", "~> 1.0"
|
|
12
|
+
gem "rubocop-minitest", "~> 0.35"
|
|
13
|
+
gem "rubocop-performance", "~> 1.0"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
group :test do
|
|
17
|
+
gem "minitest", "~> 5.0"
|
|
18
|
+
gem "sqlite3", "~> 2.1"
|
|
19
|
+
gem "ostruct"
|
|
20
|
+
gem "simplecov", require: false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -97,4 +97,8 @@ PricingPlans.configure do |config|
|
|
|
97
97
|
|
|
98
98
|
# Downgrade policy hook used by CTA ergonomics helpers
|
|
99
99
|
# config.downgrade_policy = ->(from:, to:, plan_owner:) { [true, nil] }
|
|
100
|
+
|
|
101
|
+
# Enable verbose debug logging for PricingPlans internals (Pay detection, plan resolution, etc).
|
|
102
|
+
# When set to true, detailed debug output will be printed to stdout, which can be helpful for troubleshooting.
|
|
103
|
+
# config.debug = false
|
|
100
104
|
end
|
|
@@ -9,12 +9,14 @@ module PricingPlans
|
|
|
9
9
|
attr_accessor :default_plan, :highlighted_plan, :period_cycle
|
|
10
10
|
# Optional ergonomics
|
|
11
11
|
attr_accessor :default_cta_text, :default_cta_url
|
|
12
|
+
# Debug mode - set to true to enable debug output
|
|
13
|
+
attr_accessor :debug
|
|
12
14
|
# Global controller ergonomics
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Optional global resolver for controller plan owner. Per-controller settings still win.
|
|
16
|
+
# Accepts:
|
|
17
|
+
# - Symbol: a controller helper to call (e.g., :current_organization)
|
|
18
|
+
# - Proc: instance-exec'd in the controller (self is the controller)
|
|
19
|
+
attr_reader :controller_plan_owner_method, :controller_plan_owner_proc
|
|
18
20
|
# When a limit check blocks, controllers can redirect to a global default target.
|
|
19
21
|
# Accepts:
|
|
20
22
|
# - Symbol: a controller helper to call (e.g., :pricing_path)
|
|
@@ -70,6 +72,7 @@ module PricingPlans
|
|
|
70
72
|
@free_price_caption = "Forever free"
|
|
71
73
|
@interval_default_for_ui = :month
|
|
72
74
|
@downgrade_policy = ->(from:, to:, plan_owner:) { [true, nil] }
|
|
75
|
+
@debug = false
|
|
73
76
|
@plans = {}
|
|
74
77
|
@event_handlers = {
|
|
75
78
|
warning: {},
|
|
@@ -180,6 +183,11 @@ module PricingPlans
|
|
|
180
183
|
if @highlighted_plan && !@plans.key?(@highlighted_plan)
|
|
181
184
|
raise PricingPlans::ConfigurationError, "highlighted_plan #{@highlighted_plan} is not defined"
|
|
182
185
|
end
|
|
186
|
+
|
|
187
|
+
# Highlighted plan cannot be hidden (would never appear in pricing pages)
|
|
188
|
+
if @highlighted_plan && @plans[@highlighted_plan]&.hidden?
|
|
189
|
+
raise PricingPlans::ConfigurationError, "highlighted_plan #{@highlighted_plan} cannot be hidden"
|
|
190
|
+
end
|
|
183
191
|
end
|
|
184
192
|
|
|
185
193
|
def validate_plans!
|
|
@@ -188,9 +188,10 @@ module PricingPlans
|
|
|
188
188
|
plan = PlanResolver.effective_plan_for(plan_owner)
|
|
189
189
|
limit_config = plan&.limit_for(limit_key)
|
|
190
190
|
|
|
191
|
-
# If no limit is configured,
|
|
191
|
+
# BREAKING CHANGE: If no limit is configured, block the action (secure by default)
|
|
192
|
+
# Previously this returned :within (allowed), now returns :blocked
|
|
192
193
|
unless limit_config
|
|
193
|
-
return Result.
|
|
194
|
+
return Result.blocked("Limit #{limit_key.to_s.humanize.downcase} not configured on this plan")
|
|
194
195
|
end
|
|
195
196
|
|
|
196
197
|
# Check if unlimited
|
|
@@ -20,7 +20,9 @@ module PricingPlans
|
|
|
20
20
|
def remaining(plan_owner, limit_key)
|
|
21
21
|
plan = PlanResolver.effective_plan_for(plan_owner)
|
|
22
22
|
limit_config = plan&.limit_for(limit_key)
|
|
23
|
-
|
|
23
|
+
# BREAKING CHANGE: Undefined limits now default to 0 (blocked) instead of :unlimited
|
|
24
|
+
# This is secure-by-default: if a plan doesn't define a limit, access is blocked
|
|
25
|
+
return 0 unless limit_config
|
|
24
26
|
|
|
25
27
|
limit_amount = limit_config[:to]
|
|
26
28
|
return :unlimited if limit_amount == :unlimited
|
|
@@ -54,7 +56,8 @@ module PricingPlans
|
|
|
54
56
|
def limit_amount(plan_owner, limit_key)
|
|
55
57
|
plan = PlanResolver.effective_plan_for(plan_owner)
|
|
56
58
|
limit_config = plan&.limit_for(limit_key)
|
|
57
|
-
|
|
59
|
+
# BREAKING CHANGE: Undefined limits now default to 0 (blocked) instead of :unlimited
|
|
60
|
+
return 0 unless limit_config
|
|
58
61
|
|
|
59
62
|
limit_config[:to]
|
|
60
63
|
end
|
|
@@ -189,10 +189,17 @@ module PricingPlans
|
|
|
189
189
|
|
|
190
190
|
return unless plan_owner_instance
|
|
191
191
|
|
|
192
|
-
#
|
|
192
|
+
# BREAKING CHANGE: Block when limit is not configured (secure by default)
|
|
193
193
|
plan = PlanResolver.effective_plan_for(plan_owner_instance)
|
|
194
194
|
limit_config = plan&.limit_for(limit_key)
|
|
195
|
-
|
|
195
|
+
|
|
196
|
+
# If limit is not configured, block creation (secure by default)
|
|
197
|
+
unless limit_config
|
|
198
|
+
message = error_after_limit || "Cannot create #{self.class.name.downcase}: #{limit_key.to_s.humanize.downcase} limit not configured on this plan"
|
|
199
|
+
errors.add(:base, message)
|
|
200
|
+
return
|
|
201
|
+
end
|
|
202
|
+
|
|
196
203
|
return if limit_config[:to] == :unlimited
|
|
197
204
|
|
|
198
205
|
# For persistent caps, check if we'd exceed the limit
|
|
@@ -8,78 +8,122 @@ module PricingPlans
|
|
|
8
8
|
defined?(Pay)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def log_debug(message)
|
|
12
|
+
puts message if PricingPlans.configuration&.debug
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def subscription_active_for?(plan_owner)
|
|
12
16
|
return false unless plan_owner
|
|
13
17
|
|
|
18
|
+
log_debug "[PricingPlans::PaySupport] subscription_active_for? called for #{plan_owner.class.name}##{plan_owner.id}"
|
|
19
|
+
|
|
14
20
|
# Prefer Pay's official API on the payment_processor
|
|
15
21
|
if plan_owner.respond_to?(:payment_processor) && (pp = plan_owner.payment_processor)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
log_debug "[PricingPlans::PaySupport] payment_processor found: #{pp.class.name}##{pp.id}"
|
|
23
|
+
|
|
24
|
+
# Check all subscriptions, not just the default-named one
|
|
25
|
+
# Note: Don't call pp.subscribed?() without a name parameter, as it defaults to
|
|
26
|
+
# checking only for subscriptions named Pay.default_product_name (usually "default")
|
|
27
|
+
if pp.respond_to?(:subscriptions)
|
|
28
|
+
subs = pp.subscriptions
|
|
29
|
+
log_debug "[PricingPlans::PaySupport] subscriptions relation: #{subs.class.name}, count: #{subs.count}"
|
|
30
|
+
|
|
31
|
+
# Force array conversion to ensure we iterate through all subscriptions
|
|
32
|
+
# Some ActiveRecord relations might not enumerate properly in boolean context
|
|
33
|
+
subs_array = subs.respond_to?(:to_a) ? subs.to_a : subs
|
|
34
|
+
log_debug "[PricingPlans::PaySupport] subscriptions array size: #{subs_array.size}"
|
|
35
|
+
|
|
36
|
+
subs_array.each_with_index do |sub, idx|
|
|
37
|
+
log_debug "[PricingPlans::PaySupport] [#{idx}] Subscription: #{sub.class.name}##{sub.id}, name: #{sub.name rescue 'N/A'}, status: #{sub.status rescue 'N/A'}, active?: #{sub.active? rescue 'N/A'}, on_trial?: #{sub.on_trial? rescue 'N/A'}, on_grace_period?: #{sub.on_grace_period? rescue 'N/A'}"
|
|
38
|
+
end
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
result = subs_array.any? { |sub| (sub.respond_to?(:active?) && sub.active?) || (sub.respond_to?(:on_trial?) && sub.on_trial?) || (sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) }
|
|
41
|
+
log_debug "[PricingPlans::PaySupport] subscription_active_for? returning: #{result}"
|
|
42
|
+
return result
|
|
43
|
+
else
|
|
44
|
+
log_debug "[PricingPlans::PaySupport] payment_processor does not respond to :subscriptions"
|
|
22
45
|
end
|
|
46
|
+
else
|
|
47
|
+
log_debug "[PricingPlans::PaySupport] No payment_processor found or plan_owner doesn't respond to :payment_processor"
|
|
23
48
|
end
|
|
24
49
|
|
|
25
50
|
# Fallbacks for apps that surface Pay state on the owner
|
|
26
51
|
individual_active = (plan_owner.respond_to?(:subscribed?) && plan_owner.subscribed?) ||
|
|
27
52
|
(plan_owner.respond_to?(:on_trial?) && plan_owner.on_trial?) ||
|
|
28
53
|
(plan_owner.respond_to?(:on_grace_period?) && plan_owner.on_grace_period?)
|
|
54
|
+
log_debug "[PricingPlans::PaySupport] Fallback individual_active: #{individual_active}"
|
|
29
55
|
return true if individual_active
|
|
30
56
|
|
|
31
57
|
if plan_owner.respond_to?(:subscriptions) && (subs = plan_owner.subscriptions)
|
|
58
|
+
log_debug "[PricingPlans::PaySupport] Checking plan_owner.subscriptions fallback"
|
|
32
59
|
return subs.any? { |sub| (sub.respond_to?(:active?) && sub.active?) || (sub.respond_to?(:on_trial?) && sub.on_trial?) || (sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) }
|
|
33
60
|
end
|
|
34
61
|
|
|
62
|
+
log_debug "[PricingPlans::PaySupport] subscription_active_for? returning false (no active subscription found)"
|
|
35
63
|
false
|
|
36
64
|
end
|
|
37
65
|
|
|
38
66
|
def current_subscription_for(plan_owner)
|
|
39
67
|
return nil unless pay_available?
|
|
40
68
|
|
|
69
|
+
log_debug "[PricingPlans::PaySupport] current_subscription_for called for #{plan_owner.class.name}##{plan_owner.id}"
|
|
70
|
+
|
|
41
71
|
# Prefer Pay's payment_processor API
|
|
42
72
|
if plan_owner.respond_to?(:payment_processor) && (pp = plan_owner.payment_processor)
|
|
43
|
-
|
|
44
|
-
subscription = pp.subscription
|
|
45
|
-
if subscription && (
|
|
46
|
-
(subscription.respond_to?(:active?) && subscription.active?) ||
|
|
47
|
-
(subscription.respond_to?(:on_trial?) && subscription.on_trial?) ||
|
|
48
|
-
(subscription.respond_to?(:on_grace_period?) && subscription.on_grace_period?)
|
|
49
|
-
)
|
|
50
|
-
return subscription
|
|
51
|
-
end
|
|
52
|
-
end
|
|
73
|
+
log_debug "[PricingPlans::PaySupport] payment_processor found: #{pp.class.name}##{pp.id}"
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
# Check all subscriptions, not just the default-named one
|
|
76
|
+
# Note: Don't call pp.subscription() without a name parameter, as it defaults to
|
|
77
|
+
# looking for subscriptions named Pay.default_product_name (usually "default")
|
|
78
|
+
if pp.respond_to?(:subscriptions)
|
|
79
|
+
subs = pp.subscriptions
|
|
80
|
+
log_debug "[PricingPlans::PaySupport] subscriptions relation: #{subs.class.name}, count: #{subs.count}"
|
|
81
|
+
|
|
82
|
+
# Force array conversion to ensure we iterate properly
|
|
83
|
+
subs_array = subs.respond_to?(:to_a) ? subs.to_a : subs
|
|
84
|
+
log_debug "[PricingPlans::PaySupport] subscriptions array size: #{subs_array.size}"
|
|
85
|
+
|
|
86
|
+
found = subs_array.find do |sub|
|
|
56
87
|
(sub.respond_to?(:on_trial?) && sub.on_trial?) ||
|
|
57
88
|
(sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) ||
|
|
58
89
|
(sub.respond_to?(:active?) && sub.active?)
|
|
59
90
|
end
|
|
91
|
+
log_debug "[PricingPlans::PaySupport] current_subscription_for found: #{found ? "#{found.class.name}##{found.id} (name: #{found.name})" : 'nil'}"
|
|
60
92
|
return found if found
|
|
93
|
+
else
|
|
94
|
+
log_debug "[PricingPlans::PaySupport] payment_processor does not respond to :subscriptions"
|
|
61
95
|
end
|
|
96
|
+
else
|
|
97
|
+
log_debug "[PricingPlans::PaySupport] No payment_processor found or plan_owner doesn't respond to :payment_processor"
|
|
62
98
|
end
|
|
63
99
|
|
|
64
100
|
# Fallbacks for apps that surface subscriptions on the owner
|
|
65
101
|
if plan_owner.respond_to?(:subscription)
|
|
102
|
+
log_debug "[PricingPlans::PaySupport] Checking plan_owner.subscription fallback"
|
|
66
103
|
subscription = plan_owner.subscription
|
|
67
104
|
if subscription && (
|
|
68
105
|
(subscription.respond_to?(:active?) && subscription.active?) ||
|
|
69
106
|
(subscription.respond_to?(:on_trial?) && subscription.on_trial?) ||
|
|
70
107
|
(subscription.respond_to?(:on_grace_period?) && subscription.on_grace_period?)
|
|
71
108
|
)
|
|
109
|
+
log_debug "[PricingPlans::PaySupport] current_subscription_for returning fallback subscription"
|
|
72
110
|
return subscription
|
|
73
111
|
end
|
|
74
112
|
end
|
|
75
113
|
|
|
76
114
|
if plan_owner.respond_to?(:subscriptions) && (subs = plan_owner.subscriptions)
|
|
77
|
-
|
|
115
|
+
log_debug "[PricingPlans::PaySupport] Checking plan_owner.subscriptions fallback"
|
|
116
|
+
found = subs.find do |sub|
|
|
78
117
|
(sub.respond_to?(:on_trial?) && sub.on_trial?) ||
|
|
79
118
|
(sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) ||
|
|
80
119
|
(sub.respond_to?(:active?) && sub.active?)
|
|
81
120
|
end
|
|
121
|
+
log_debug "[PricingPlans::PaySupport] current_subscription_for found in fallback: #{found ? "#{found.class.name}##{found.id}" : 'nil'}"
|
|
122
|
+
return found if found
|
|
82
123
|
end
|
|
124
|
+
|
|
125
|
+
log_debug "[PricingPlans::PaySupport] current_subscription_for returning nil"
|
|
126
|
+
nil
|
|
83
127
|
end
|
|
84
128
|
end
|
|
85
129
|
end
|
data/lib/pricing_plans/plan.rb
CHANGED
|
@@ -24,6 +24,7 @@ module PricingPlans
|
|
|
24
24
|
@cta_url = nil
|
|
25
25
|
@default = false
|
|
26
26
|
@highlighted = false
|
|
27
|
+
@hidden = false
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# DSL methods for plan configuration
|
|
@@ -138,6 +139,9 @@ module PricingPlans
|
|
|
138
139
|
end
|
|
139
140
|
end
|
|
140
141
|
|
|
142
|
+
alias_method :set_metadata, :set_meta
|
|
143
|
+
alias_method :metadata, :meta
|
|
144
|
+
|
|
141
145
|
# CTA helpers for pricing UI
|
|
142
146
|
def set_cta_text(value)
|
|
143
147
|
@cta_text = value&.to_s
|
|
@@ -168,7 +172,7 @@ module PricingPlans
|
|
|
168
172
|
default = PricingPlans.configuration.default_cta_url
|
|
169
173
|
return default if default
|
|
170
174
|
# New default: if host app defines subscribe_path, prefer that
|
|
171
|
-
if defined?(Rails) && Rails.application.routes.url_helpers.respond_to?(:subscribe_path)
|
|
175
|
+
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application && Rails.application.routes.url_helpers.respond_to?(:subscribe_path)
|
|
172
176
|
return Rails.application.routes.url_helpers.subscribe_path(plan: key, interval: :month)
|
|
173
177
|
end
|
|
174
178
|
nil
|
|
@@ -275,6 +279,14 @@ module PricingPlans
|
|
|
275
279
|
false
|
|
276
280
|
end
|
|
277
281
|
|
|
282
|
+
def hidden!(value = true)
|
|
283
|
+
@hidden = !!value
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def hidden?
|
|
287
|
+
!!@hidden
|
|
288
|
+
end
|
|
289
|
+
|
|
278
290
|
# Syntactic sugar for popular/highlighted
|
|
279
291
|
def popular?
|
|
280
292
|
highlighted?
|
|
@@ -471,6 +483,7 @@ module PricingPlans
|
|
|
471
483
|
name: name,
|
|
472
484
|
description: description,
|
|
473
485
|
features: bullets, # alias in this gem
|
|
486
|
+
metadata: metadata.dup,
|
|
474
487
|
highlighted: highlighted?,
|
|
475
488
|
default: default?,
|
|
476
489
|
free: free?,
|
|
@@ -3,24 +3,44 @@
|
|
|
3
3
|
module PricingPlans
|
|
4
4
|
class PlanResolver
|
|
5
5
|
class << self
|
|
6
|
+
def log_debug(message)
|
|
7
|
+
puts message if PricingPlans.configuration&.debug
|
|
8
|
+
end
|
|
9
|
+
|
|
6
10
|
def effective_plan_for(plan_owner)
|
|
11
|
+
log_debug "[PricingPlans::PlanResolver] effective_plan_for called for #{plan_owner.class.name}##{plan_owner.respond_to?(:id) ? plan_owner.id : 'N/A'}"
|
|
12
|
+
|
|
7
13
|
# 1. Check Pay subscription status first (no app-specific gate required)
|
|
8
|
-
|
|
14
|
+
pay_available = PaySupport.pay_available?
|
|
15
|
+
log_debug "[PricingPlans::PlanResolver] PaySupport.pay_available? = #{pay_available}"
|
|
16
|
+
log_debug "[PricingPlans::PlanResolver] defined?(Pay) = #{defined?(Pay)}"
|
|
17
|
+
|
|
18
|
+
if pay_available
|
|
19
|
+
log_debug "[PricingPlans::PlanResolver] Calling resolve_plan_from_pay..."
|
|
9
20
|
plan_from_pay = resolve_plan_from_pay(plan_owner)
|
|
21
|
+
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay returned: #{plan_from_pay ? plan_from_pay.key : 'nil'}"
|
|
10
22
|
return plan_from_pay if plan_from_pay
|
|
11
23
|
end
|
|
12
24
|
|
|
13
25
|
# 2. Check manual assignment
|
|
26
|
+
log_debug "[PricingPlans::PlanResolver] Checking for manual assignment..."
|
|
14
27
|
if plan_owner.respond_to?(:id)
|
|
15
28
|
assignment = Assignment.find_by(
|
|
16
29
|
plan_owner_type: plan_owner.class.name,
|
|
17
30
|
plan_owner_id: plan_owner.id
|
|
18
31
|
)
|
|
19
|
-
|
|
32
|
+
if assignment
|
|
33
|
+
log_debug "[PricingPlans::PlanResolver] Found manual assignment: #{assignment.plan_key}"
|
|
34
|
+
return Registry.plan(assignment.plan_key)
|
|
35
|
+
else
|
|
36
|
+
log_debug "[PricingPlans::PlanResolver] No manual assignment found"
|
|
37
|
+
end
|
|
20
38
|
end
|
|
21
39
|
|
|
22
40
|
# 3. Fall back to default plan
|
|
23
|
-
Registry.default_plan
|
|
41
|
+
default = Registry.default_plan
|
|
42
|
+
log_debug "[PricingPlans::PlanResolver] Returning default plan: #{default ? default.key : 'nil'}"
|
|
43
|
+
default
|
|
24
44
|
end
|
|
25
45
|
|
|
26
46
|
def plan_key_for(plan_owner)
|
|
@@ -43,21 +63,44 @@ module PricingPlans
|
|
|
43
63
|
end
|
|
44
64
|
|
|
45
65
|
def resolve_plan_from_pay(plan_owner)
|
|
46
|
-
|
|
66
|
+
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay: checking if plan_owner has payment_processor or Pay methods..."
|
|
67
|
+
|
|
68
|
+
# Check if plan_owner has payment_processor (preferred) or Pay methods directly (fallback)
|
|
69
|
+
has_payment_processor = plan_owner.respond_to?(:payment_processor)
|
|
70
|
+
has_pay_methods = plan_owner.respond_to?(:subscribed?) ||
|
|
47
71
|
plan_owner.respond_to?(:on_trial?) ||
|
|
48
72
|
plan_owner.respond_to?(:on_grace_period?) ||
|
|
49
73
|
plan_owner.respond_to?(:subscriptions)
|
|
50
74
|
|
|
75
|
+
log_debug "[PricingPlans::PlanResolver] has_payment_processor? #{has_payment_processor}"
|
|
76
|
+
log_debug "[PricingPlans::PlanResolver] has_pay_methods? #{has_pay_methods}"
|
|
77
|
+
|
|
78
|
+
# PaySupport will handle both payment_processor and direct Pay methods
|
|
79
|
+
return nil unless has_payment_processor || has_pay_methods
|
|
80
|
+
|
|
51
81
|
# Check if plan_owner has active subscription, trial, or grace period
|
|
52
|
-
|
|
82
|
+
log_debug "[PricingPlans::PlanResolver] Calling PaySupport.subscription_active_for?..."
|
|
83
|
+
is_active = PaySupport.subscription_active_for?(plan_owner)
|
|
84
|
+
log_debug "[PricingPlans::PlanResolver] subscription_active_for? returned: #{is_active}"
|
|
85
|
+
|
|
86
|
+
if is_active
|
|
87
|
+
log_debug "[PricingPlans::PlanResolver] Calling PaySupport.current_subscription_for..."
|
|
53
88
|
subscription = PaySupport.current_subscription_for(plan_owner)
|
|
89
|
+
log_debug "[PricingPlans::PlanResolver] current_subscription_for returned: #{subscription ? subscription.class.name : 'nil'}"
|
|
54
90
|
return nil unless subscription
|
|
55
91
|
|
|
56
92
|
# Map processor plan to our plan
|
|
57
93
|
processor_plan = subscription.processor_plan
|
|
58
|
-
|
|
94
|
+
log_debug "[PricingPlans::PlanResolver] subscription.processor_plan = #{processor_plan.inspect}"
|
|
95
|
+
|
|
96
|
+
if processor_plan
|
|
97
|
+
matched_plan = plan_from_processor_plan(processor_plan)
|
|
98
|
+
log_debug "[PricingPlans::PlanResolver] plan_from_processor_plan returned: #{matched_plan ? matched_plan.key : 'nil'}"
|
|
99
|
+
return matched_plan
|
|
100
|
+
end
|
|
59
101
|
end
|
|
60
102
|
|
|
103
|
+
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay returning nil"
|
|
61
104
|
nil
|
|
62
105
|
end
|
|
63
106
|
|
data/lib/pricing_plans.rb
CHANGED
|
@@ -77,8 +77,10 @@ module PricingPlans
|
|
|
77
77
|
|
|
78
78
|
# Zero-shim Plans API for host apps
|
|
79
79
|
# Returns an array of Plan objects in a sensible order (free → paid → enterprise/contact)
|
|
80
|
+
# Excludes hidden plans (use Registry.plans to access all plans including hidden)
|
|
80
81
|
def plans
|
|
81
82
|
array = Registry.plans.values
|
|
83
|
+
.reject(&:hidden?) # Filter out hidden plans from public API
|
|
82
84
|
array.sort_by do |p|
|
|
83
85
|
# Free first, then numeric price ascending, then price_string/stripe-price at the end
|
|
84
86
|
if p.price && p.price.to_f.zero?
|
|
@@ -106,9 +108,10 @@ module PricingPlans
|
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
# Opinionated next-plan suggestion: pick the smallest plan that satisfies current usage
|
|
111
|
+
# Always suggests visible plans only (never suggests hidden plans)
|
|
109
112
|
def suggest_next_plan_for(plan_owner, keys: nil)
|
|
110
113
|
current_plan = PlanResolver.effective_plan_for(plan_owner)
|
|
111
|
-
sorted = plans
|
|
114
|
+
sorted = plans # Only visible plans (hidden plans filtered out)
|
|
112
115
|
keys ||= (current_plan&.limits&.keys || [])
|
|
113
116
|
keys = keys.map(&:to_sym)
|
|
114
117
|
|
|
@@ -122,7 +125,9 @@ module PricingPlans
|
|
|
122
125
|
limit[:to] == :unlimited || LimitChecker.current_usage_for(plan_owner, key, limit) <= limit[:to].to_i
|
|
123
126
|
end
|
|
124
127
|
end
|
|
125
|
-
|
|
128
|
+
# Fallback logic: never suggest hidden plans
|
|
129
|
+
# If current_plan is hidden (e.g., :unsubscribed), suggest first visible plan instead
|
|
130
|
+
candidate || (current_plan unless current_plan&.hidden?) || sorted.first
|
|
126
131
|
end
|
|
127
132
|
|
|
128
133
|
# Optional view-model decorator for UIs (pure data, no HTML)
|
|
@@ -130,11 +135,13 @@ module PricingPlans
|
|
|
130
135
|
is_current = plan_owner ? (PlanResolver.effective_plan_for(plan_owner)&.key == plan.key) : false
|
|
131
136
|
is_popular = Registry.highlighted_plan&.key == plan.key
|
|
132
137
|
price_label = plan_price_label_for(plan)
|
|
138
|
+
# Duplicate metadata to avoid mutating plan internals from view-layer code.
|
|
133
139
|
{
|
|
134
140
|
key: plan.key,
|
|
135
141
|
name: plan.name,
|
|
136
142
|
description: plan.description,
|
|
137
143
|
bullets: plan.bullets,
|
|
144
|
+
metadata: plan.metadata.dup,
|
|
138
145
|
price_label: price_label,
|
|
139
146
|
is_current: is_current,
|
|
140
147
|
is_popular: is_popular,
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pricing_plans
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-01-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -49,104 +49,6 @@ dependencies:
|
|
|
49
49
|
- - "<"
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
51
|
version: '9.0'
|
|
52
|
-
- !ruby/object:Gem::Dependency
|
|
53
|
-
name: bundler
|
|
54
|
-
requirement: !ruby/object:Gem::Requirement
|
|
55
|
-
requirements:
|
|
56
|
-
- - "~>"
|
|
57
|
-
- !ruby/object:Gem::Version
|
|
58
|
-
version: '2.0'
|
|
59
|
-
type: :development
|
|
60
|
-
prerelease: false
|
|
61
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
-
requirements:
|
|
63
|
-
- - "~>"
|
|
64
|
-
- !ruby/object:Gem::Version
|
|
65
|
-
version: '2.0'
|
|
66
|
-
- !ruby/object:Gem::Dependency
|
|
67
|
-
name: rake
|
|
68
|
-
requirement: !ruby/object:Gem::Requirement
|
|
69
|
-
requirements:
|
|
70
|
-
- - "~>"
|
|
71
|
-
- !ruby/object:Gem::Version
|
|
72
|
-
version: '13.0'
|
|
73
|
-
type: :development
|
|
74
|
-
prerelease: false
|
|
75
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
-
requirements:
|
|
77
|
-
- - "~>"
|
|
78
|
-
- !ruby/object:Gem::Version
|
|
79
|
-
version: '13.0'
|
|
80
|
-
- !ruby/object:Gem::Dependency
|
|
81
|
-
name: minitest
|
|
82
|
-
requirement: !ruby/object:Gem::Requirement
|
|
83
|
-
requirements:
|
|
84
|
-
- - "~>"
|
|
85
|
-
- !ruby/object:Gem::Version
|
|
86
|
-
version: '5.0'
|
|
87
|
-
type: :development
|
|
88
|
-
prerelease: false
|
|
89
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
-
requirements:
|
|
91
|
-
- - "~>"
|
|
92
|
-
- !ruby/object:Gem::Version
|
|
93
|
-
version: '5.0'
|
|
94
|
-
- !ruby/object:Gem::Dependency
|
|
95
|
-
name: sqlite3
|
|
96
|
-
requirement: !ruby/object:Gem::Requirement
|
|
97
|
-
requirements:
|
|
98
|
-
- - "~>"
|
|
99
|
-
- !ruby/object:Gem::Version
|
|
100
|
-
version: '2.1'
|
|
101
|
-
type: :development
|
|
102
|
-
prerelease: false
|
|
103
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
-
requirements:
|
|
105
|
-
- - "~>"
|
|
106
|
-
- !ruby/object:Gem::Version
|
|
107
|
-
version: '2.1'
|
|
108
|
-
- !ruby/object:Gem::Dependency
|
|
109
|
-
name: rubocop
|
|
110
|
-
requirement: !ruby/object:Gem::Requirement
|
|
111
|
-
requirements:
|
|
112
|
-
- - "~>"
|
|
113
|
-
- !ruby/object:Gem::Version
|
|
114
|
-
version: '1.0'
|
|
115
|
-
type: :development
|
|
116
|
-
prerelease: false
|
|
117
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
-
requirements:
|
|
119
|
-
- - "~>"
|
|
120
|
-
- !ruby/object:Gem::Version
|
|
121
|
-
version: '1.0'
|
|
122
|
-
- !ruby/object:Gem::Dependency
|
|
123
|
-
name: rubocop-minitest
|
|
124
|
-
requirement: !ruby/object:Gem::Requirement
|
|
125
|
-
requirements:
|
|
126
|
-
- - "~>"
|
|
127
|
-
- !ruby/object:Gem::Version
|
|
128
|
-
version: '0.35'
|
|
129
|
-
type: :development
|
|
130
|
-
prerelease: false
|
|
131
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
-
requirements:
|
|
133
|
-
- - "~>"
|
|
134
|
-
- !ruby/object:Gem::Version
|
|
135
|
-
version: '0.35'
|
|
136
|
-
- !ruby/object:Gem::Dependency
|
|
137
|
-
name: rubocop-performance
|
|
138
|
-
requirement: !ruby/object:Gem::Requirement
|
|
139
|
-
requirements:
|
|
140
|
-
- - "~>"
|
|
141
|
-
- !ruby/object:Gem::Version
|
|
142
|
-
version: '1.0'
|
|
143
|
-
type: :development
|
|
144
|
-
prerelease: false
|
|
145
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
146
|
-
requirements:
|
|
147
|
-
- - "~>"
|
|
148
|
-
- !ruby/object:Gem::Version
|
|
149
|
-
version: '1.0'
|
|
150
52
|
description: Define and enforce pricing plan limits in your Rails SaaS (entitlements,
|
|
151
53
|
quotas, feature gating). pricing_plans acts as your single source of truth for pricing
|
|
152
54
|
plans. Define a pricing catalog with feature gating, persistent caps, per‑period
|
|
@@ -161,6 +63,7 @@ extra_rdoc_files: []
|
|
|
161
63
|
files:
|
|
162
64
|
- ".claude/settings.local.json"
|
|
163
65
|
- ".rubocop.yml"
|
|
66
|
+
- Appraisals
|
|
164
67
|
- CHANGELOG.md
|
|
165
68
|
- CLAUDE.md
|
|
166
69
|
- LICENSE.txt
|
|
@@ -176,6 +79,8 @@ files:
|
|
|
176
79
|
- docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg
|
|
177
80
|
- docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg
|
|
178
81
|
- docs/images/product_creation_blocked.jpg
|
|
82
|
+
- gemfiles/rails_7.2.gemfile
|
|
83
|
+
- gemfiles/rails_8.1.gemfile
|
|
179
84
|
- lib/generators/pricing_plans/install/install_generator.rb
|
|
180
85
|
- lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb
|
|
181
86
|
- lib/generators/pricing_plans/install/templates/initializer.rb
|