pricing_plans 0.1.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.local.json +16 -0
  3. data/.rubocop.yml +137 -0
  4. data/CHANGELOG.md +83 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +241 -0
  7. data/Rakefile +15 -0
  8. data/docs/01-define-pricing-plans.md +372 -0
  9. data/docs/02-controller-helpers.md +223 -0
  10. data/docs/03-model-helpers.md +318 -0
  11. data/docs/04-views.md +121 -0
  12. data/docs/05-semantic-pricing.md +159 -0
  13. data/docs/06-gem-compatibility.md +99 -0
  14. data/docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg +0 -0
  15. data/docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg +0 -0
  16. data/docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg +0 -0
  17. data/docs/images/product_creation_blocked.jpg +0 -0
  18. data/lib/generators/pricing_plans/install/install_generator.rb +42 -0
  19. data/lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb +91 -0
  20. data/lib/generators/pricing_plans/install/templates/initializer.rb +100 -0
  21. data/lib/pricing_plans/association_limit_registry.rb +45 -0
  22. data/lib/pricing_plans/configuration.rb +189 -0
  23. data/lib/pricing_plans/controller_guards.rb +574 -0
  24. data/lib/pricing_plans/controller_rescues.rb +115 -0
  25. data/lib/pricing_plans/dsl.rb +44 -0
  26. data/lib/pricing_plans/engine.rb +69 -0
  27. data/lib/pricing_plans/grace_manager.rb +227 -0
  28. data/lib/pricing_plans/integer_refinements.rb +48 -0
  29. data/lib/pricing_plans/job_guards.rb +24 -0
  30. data/lib/pricing_plans/limit_checker.rb +157 -0
  31. data/lib/pricing_plans/limitable.rb +286 -0
  32. data/lib/pricing_plans/models/assignment.rb +55 -0
  33. data/lib/pricing_plans/models/enforcement_state.rb +45 -0
  34. data/lib/pricing_plans/models/usage.rb +51 -0
  35. data/lib/pricing_plans/overage_reporter.rb +77 -0
  36. data/lib/pricing_plans/pay_support.rb +85 -0
  37. data/lib/pricing_plans/period_calculator.rb +183 -0
  38. data/lib/pricing_plans/plan.rb +653 -0
  39. data/lib/pricing_plans/plan_owner.rb +287 -0
  40. data/lib/pricing_plans/plan_resolver.rb +85 -0
  41. data/lib/pricing_plans/price_components.rb +16 -0
  42. data/lib/pricing_plans/registry.rb +182 -0
  43. data/lib/pricing_plans/result.rb +109 -0
  44. data/lib/pricing_plans/version.rb +5 -0
  45. data/lib/pricing_plans/view_helpers.rb +58 -0
  46. data/lib/pricing_plans.rb +645 -0
  47. data/sig/pricing_plans.rbs +4 -0
  48. metadata +236 -0
@@ -0,0 +1,372 @@
1
+ # Define pricing plans in `pricing_plans.rb`
2
+
3
+ You define plans and their limits and features as code in the `pricing_plans.rb` initializer. The `pricing_plans` offers you a DSL that makes plan definition intuitive and read like plain English.
4
+
5
+ To define a free plan, for example, you would do:
6
+
7
+ ```ruby
8
+ PricingPlans.configure do |config|
9
+ plan :free do
10
+ price 0
11
+ default!
12
+ end
13
+ end
14
+ ```
15
+
16
+ That's the basics! Let's dive in.
17
+
18
+ > [!IMPORTANT]
19
+ > You must set a default plan (either mark one with `default!` in the plan DSL or set `config.default_plan = :your_plan_key`).
20
+
21
+ ## Define what each plan gives
22
+
23
+ At a high level, a plan needs to do **two** things:
24
+ (1) Gate features
25
+ (2) Enforce limits (quotas)
26
+
27
+ ## (1) Gate features in a plan
28
+
29
+ Let's start by giving access to certain features. For example, our free plan could give users API access:
30
+
31
+ ```ruby
32
+ PricingPlans.configure do |config|
33
+ plan :free do
34
+ price 0
35
+
36
+ allows :api_access
37
+ end
38
+ end
39
+ ```
40
+
41
+ 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
+
43
+
44
+ All features are disabled by default unless explicitly made available with the `allows` keyword. However, for clarity we can explicitly say what the plan disallows:
45
+
46
+ ```ruby
47
+ PricingPlans.configure do |config|
48
+ plan :free do
49
+ price 0
50
+
51
+ allows :api_access
52
+ disallows :premium_features
53
+ end
54
+ end
55
+ ```
56
+
57
+ This wouldn't do anything, though, because all features are disabled by default; but it makes it obvious what the plan does and doesn't.
58
+
59
+ ## (2) Enforce limits (quotas) in a plan
60
+
61
+ The other thing plans can do is enforce a limit. We can define limits like this:
62
+
63
+ ```ruby
64
+ PricingPlans.configure do |config|
65
+ plan :free do
66
+ price 0
67
+
68
+ allows :api_access
69
+
70
+ limits :projects, to: 3
71
+ end
72
+ end
73
+ ```
74
+
75
+ The `limits :projects, to: 3` does exactly that: whoever has this plan can only have three projects at most. We'll see later [how to tie this limit to the actual model relationship](#models), but for now, we're just **defining** the limit.
76
+
77
+ ### `after_limit`: Define what happens after a limit is reached
78
+
79
+ What happens after a limit is reached is controlled by `after_limit`. The default is `:block_usage`. You can customize per limit. Examples:
80
+
81
+ ```ruby
82
+ # Just warn (never block):
83
+ PricingPlans.configure do |config|
84
+ plan :free do
85
+ price 0
86
+ allows :api_access
87
+ limits :projects, to: 3, after_limit: :just_warn
88
+ end
89
+ end
90
+ ```
91
+
92
+ If we want to prevent more resources being created after the limit has been reached, we can `:block_usage`:
93
+
94
+ ```ruby
95
+ # Block immediately:
96
+ PricingPlans.configure do |config|
97
+ plan :free do
98
+ price 0
99
+ allows :api_access
100
+ limits :projects, to: 3, after_limit: :block_usage
101
+ end
102
+ end
103
+ ```
104
+
105
+ However, we can be nicer and give users a bit of a grace period after the limit has been reached. To do that, we use `:grace_then_block`:
106
+
107
+ ```ruby
108
+ # Opt into grace, then block:
109
+ PricingPlans.configure do |config|
110
+ plan :free do
111
+ price 0
112
+ allows :api_access
113
+ limits :projects, to: 3, after_limit: :grace_then_block
114
+ end
115
+ end
116
+ ```
117
+
118
+ We can also specify how long the grace period is:
119
+
120
+ ```ruby
121
+ PricingPlans.configure do |config|
122
+ plan :free do
123
+ price 0
124
+ allows :api_access
125
+ limits :projects, to: 3, after_limit: :grace_then_block, grace: 7.days
126
+ end
127
+ end
128
+ ```
129
+
130
+ In summary: persistent caps count live rows (per plan owner model). When over the cap:
131
+ - `:just_warn` → validation passes; use controller guard to warn.
132
+ - `:block_usage` → validation fails immediately (uses `error_after_limit` if set).
133
+ - `:grace_then_block` → validation fails once grace is considered “blocked” (we track and switch from grace to blocked).
134
+
135
+ Note: `grace` is only valid with blocking behaviors. We’ll raise at boot if you set `grace` with `:just_warn`.
136
+
137
+ ### Per‑period allowances
138
+
139
+ Besides persistent caps, a limit can be defined as a per‑period allowance that resets each window. Example:
140
+
141
+ ```ruby
142
+ plan :pro do
143
+ # Allow up to 3 custom models per calendar month
144
+ limits :custom_models, to: 3, per: :calendar_month
145
+ end
146
+ ```
147
+
148
+ Accepted `per:` values:
149
+ - `:billing_cycle` (default globally; respects Pay subscription anchors if available, else falls back to calendar month)
150
+ - `:calendar_month`, `:calendar_week`, `:calendar_day`
151
+ - A callable: `->(plan_owner) { [start_time, end_time] }`
152
+ - An ActiveSupport duration: `2.weeks` (window starts at beginning of day)
153
+
154
+ Per‑period usage is tracked in [the `PricingPlans::Usage` model (`pricing_plans_usages` table)](#why-the-models) and read live. Persistent caps do not use this table.
155
+
156
+ #### How period windows are calculated
157
+
158
+ - **Default period**: Controlled by `config.period_cycle` (defaults to `:billing_cycle`). You can override per limit with `per:`.
159
+ - **Billing cycle**: When `pay` is available, we use the subscription’s anchors (`current_period_start`/`current_period_end`). If not available, we fall back to a monthly window anchored at the subscription’s `created_at`. If there is no subscription, we fall back to calendar month.
160
+ - **Calendar windows**: `:calendar_month`, `:calendar_week`, `:calendar_day` map to `beginning_of_* … end_of_*` for the current time.
161
+ - **Duration windows**: For `ActiveSupport::Duration` (e.g., `2.weeks`), the window starts at `beginning_of_day` and ends at `start + duration`.
162
+ - **Custom callable**: You can pass `->(plan_owner) { [start_time, end_time] }`. We validate that both are present and `end > start`.
163
+
164
+ #### Automatic usage tracking (race‑safe)
165
+
166
+ - Include `limited_by_pricing_plans` on the model that represents the metered object. On `after_create`, we atomically upsert/increment the current period’s usage row for that `plan_owner` and `limit_key`.
167
+ - Concurrency: we de‑duplicate with a uniqueness constraint and retry on `RecordNotUnique` to increment safely.
168
+ - Reads are live: `LimitChecker.current_usage_for(plan_owner, :key)` returns the current window’s `used` (or 0 if none).
169
+
170
+ Callback timing:
171
+ - We increment usage in an `after_create` callback (not `after_commit`). This runs inside the same database transaction as the record creation, so if the outer transaction rolls back, the usage increment rolls back as well.
172
+
173
+ #### Grace/warnings and period rollover (explicit semantics)
174
+
175
+ - State lives in `pricing_plans_enforcement_states` per plan_owner+limit.
176
+ - Per‑period limits:
177
+ - We stamp the active window on the state; when the window changes, stale state is discarded automatically (warnings re‑arm and grace resets at each new window).
178
+ - Warnings: thresholds re‑arm every window; the same threshold can emit again in the next window.
179
+ - Grace: if `:grace_then_block`, grace is per window. A new window clears prior grace/blocked state.
180
+ - Persistent caps:
181
+ - Warnings are monotonic: once a higher `warn_at` threshold has been emitted, we do not re‑emit lower or equal thresholds again unless you clear state via `PricingPlans::GraceManager.reset_state!(plan_owner, :limit_key)`.
182
+ - Grace is absolute: if `:grace_then_block`, we start grace once the limit is exceeded. It expires after the configured duration. There is no automatic reset tied to time windows. Enforcement for creates is still driven by “would this action exceed the cap now?”. If usage drops below the cap, create checks will pass again even if a prior state exists.
183
+ - You may clear any existing warning/grace/blocked state manually with `reset_state!`.
184
+
185
+ #### Example: usage resets next period
186
+
187
+ ```ruby
188
+ # pro allows 3 custom models per month
189
+ PricingPlans::Assignment.assign_plan_to(org, :pro)
190
+
191
+ travel_to(Time.parse("2025-01-15 12:00:00 UTC")) do
192
+ 3.times { org.custom_models.create!(name: "Model") }
193
+ PricingPlans::LimitChecker.plan_limit_remaining(org, :custom_models)
194
+ # => 0
195
+ result = PricingPlans::ControllerGuards.require_plan_limit!(:custom_models, plan_owner: org)
196
+ result.grace? # => true when after_limit: :grace_then_block
197
+ end
198
+
199
+ travel_to(Time.parse("2025-02-01 12:00:00 UTC")) do
200
+ # New window — counters reset automatically
201
+ PricingPlans::LimitChecker.plan_limit_remaining(org, :custom_models)
202
+ # => 3
203
+ end
204
+ ```
205
+
206
+ ### Warn users when they cross a limit threshold
207
+
208
+ We can also set thresholds to warn our users when they're halfway through their limit, approaching the limit, etc. To do that, we first set up trigger thresholds with `warn_at:`
209
+
210
+ ```ruby
211
+ PricingPlans.configure do |config|
212
+ plan :free do
213
+ price 0
214
+
215
+ allows :api_access
216
+
217
+ limits :projects, to: 3, after_limit: :grace_then_block, grace: 7.days, warn_at: [0.5, 0.8, 0.95]
218
+ end
219
+ end
220
+ ```
221
+
222
+ And then, for each threshold and for each limit, an event gets triggered, and we can configure its callback in the `pricing_plans.rb` initializer:
223
+
224
+ ```ruby
225
+ config.on_warning(:projects) do |plan_owner, threshold|
226
+ # send a mail or a notification
227
+ # this fires when :projects crosses 50%, 80% and 95% of its limit
228
+ end
229
+
230
+ # Also available:
231
+ config.on_grace_start(:projects) do |plan_owner, grace_ends_at|
232
+ # notify grace started; ends at `grace_ends_at`
233
+ end
234
+ config.on_block(:projects) do |plan_owner|
235
+ # notify usage is now blocked for :projects
236
+ end
237
+ ```
238
+
239
+ If you only want a scope, like active projects, to count towards plan limits, you can do:
240
+
241
+ ```ruby
242
+ PricingPlans.configure do |config|
243
+ plan :free do
244
+ price 0
245
+
246
+ allows :api_access
247
+
248
+ limits :projects, to: 3, count_scope: :active
249
+ end
250
+ end
251
+ ```
252
+
253
+ (Assuming, of course, that your `Project` model has an `active` scope)
254
+
255
+ You can also make something unlimited (again, just syntactic sugar to be explicit, everything is unlimited unless there's an actual limit):
256
+
257
+ ```ruby
258
+ PricingPlans.configure do |config|
259
+ plan :free do
260
+ price 0
261
+
262
+ allows :api_access
263
+
264
+ unlimited :projects
265
+ end
266
+ end
267
+ ```
268
+
269
+ ### "Limits" API reference
270
+
271
+ To summarize, here's what persistent caps (plan limits) are:
272
+ - Counting is live: `SELECT COUNT(*)` scoped to the plan owner association, no counter caches.
273
+ - Validation on create: blocks immediately on `:block_usage`, or blocks when grace is considered “blocked” on `:grace_then_block`. `:just_warn` passes.
274
+ - Deletes automatically lower the count. Backfills simply reflect current rows.
275
+
276
+ - Filtered counting via count_scope: scope persistent caps to active-only rows.
277
+ - Idiomatic options:
278
+ - Plan DSL with AR Hash: `limits :licenses, to: 25, count_scope: { status: 'active' }`
279
+ - Plan DSL with named scope: `limits :activations, to: 50, count_scope: :active`
280
+ - Plan DSL with multiple: `limits :seats, to: 10, count_scope: [:active, { kind: 'paid' }]`
281
+ - Macro form on the child model: `limited_by_pricing_plans :licenses, plan_owner: :organization, count_scope: :active`
282
+ - plan_owner‑side convenience: `has_many :licenses, limited_by_pricing_plans: { limit_key: :licenses, count_scope: :active }`
283
+ - Full freedom: `->(rel) { rel.where(status: 'active') }` or `->(rel, plan_owner) { rel.where(organization_id: plan_owner.id) }`
284
+ - Accepted types: Symbol (named scope), Hash (where), Proc (arity 1 or 2), or Array of these (applied left-to-right).
285
+ - Precedence: plan-level `count_scope` overrides macro-level `count_scope`.
286
+ - Restriction: `count_scope` only applies to persistent caps (not allowed on per-period limits).
287
+ - Performance: add indexes for your filters (e.g., `status`, `deactivated_at`).
288
+
289
+
290
+ ## Define user-facing plan attributes
291
+
292
+ Since `pricing_plans.rb` is our single source of truth for plans, we can define plan information we can later use to show pricing tables, like plan name, description, and bullet points. We can also override the price for a string, and we can set a CTA button text and URL to link to:
293
+
294
+ ```ruby
295
+ PricingPlans.configure do |config|
296
+ plan :free do
297
+ price_string "Free!"
298
+
299
+ name "Free Plan" # optional, would default to "Free" as inferred from the :free key
300
+ description "A plan to get you started"
301
+ bullets "Basic features", "Community support"
302
+
303
+ cta_text "Subscribe"
304
+ # In initializers, prefer a string path/URL or set a global default CTA in config.
305
+ # Route helpers are not available here.
306
+ cta_url "/pricing"
307
+
308
+ allows :api_access
309
+
310
+ limits :projects, to: 3
311
+ end
312
+ end
313
+ ```
314
+
315
+ You can also make a plan `default!`; and you can make a plan `highlighted!` to help you when building a pricing table.
316
+
317
+
318
+ ## Link paid plans to Stripe prices (requires `pay`)
319
+
320
+ If we're defining a paid plan, and if you're already using the `pay` gem, you can omit defining the explicit price, and just let the gem read the actual price from Stripe via `pay`:
321
+
322
+ ```ruby
323
+ PricingPlans.configure do |config|
324
+ plan :pro do
325
+ stripe_price "price_123abc"
326
+
327
+ description "For growing teams and businesses"
328
+ bullets "Advanced features", "Priority support", "API access"
329
+
330
+ allows :api_access, :premium_features
331
+ limits :projects, to: 10
332
+ unlimited :team_members
333
+ highlighted!
334
+ end
335
+ end
336
+ ```
337
+
338
+ If you have monthly and yearly prices for the same plan, you can define them like:
339
+
340
+ ```ruby
341
+ PricingPlans.configure do |config|
342
+ plan :pro do
343
+ stripe_price month: "price_123abc", year: "price_456def"
344
+ end
345
+ end
346
+ ```
347
+
348
+ `stripe_price` accepts String or Hash (e.g., `{ month:, year:, id: }`) and the `pricing_plans` PlanResolver maps against Pay's `subscription.processor_plan`.
349
+
350
+
351
+ ## Example: define an enterprise plan
352
+
353
+ A common use case of pricing pages is adding a free and an enterprise plan around the regular paid plans that you may define in Stripe. The free plan is usually just a limited free tier, not associated with any external price ID; while the "Enterprise" plan may just redirect users to a sales email. To achieve this, we can do:
354
+
355
+ ```ruby
356
+ # Your free plan here
357
+
358
+ # Then your paid plans here, linked to Stripe IDs
359
+
360
+ # And finally, an enterprise plan:
361
+ plan :enterprise do
362
+ price_string "Contact"
363
+
364
+ description "Get in touch and we'll fit your needs."
365
+ bullets "Custom limits", "Dedicated SLAs", "Dedicated support"
366
+ cta_text "Contact us"
367
+ cta_url "mailto:sales@example.com"
368
+
369
+ unlimited :products
370
+ allows :api_access, :premium_features
371
+ end
372
+ ```
@@ -0,0 +1,223 @@
1
+
2
+ # Controller helpers and methods
3
+
4
+ The `pricing_plans` gem ships with controller helpers that make it easy to gate features defined in your pricing plans, and enforce limits. For these controllers methods to work, you first need to let the gem know who the current "plan owner" object is. The plan owner is the object on which the plan limits are applied (typically, the same object that gets billed for a subscription: the current user, current organization, etc.)
5
+
6
+ ## Setting things up for controllers
7
+
8
+ First of all, the gem needs a way to know what the current plan owner object is (the current user, current organization, etc.)
9
+
10
+ You can set this globally in the initializer:
11
+ ```ruby
12
+ # config/initializers/pricing_plans.rb
13
+ PricingPlans.configure do |config|
14
+ # Either:
15
+ config.controller_plan_owner :current_organization
16
+ # Or:
17
+ # config.controller_plan_owner { current_account }
18
+ end
19
+ ```
20
+
21
+ If this is not defined, `pricing_plans` will [auto-try common conventions](/lib/pricing_plans/controller_guards.rb):
22
+ - `current_organization`
23
+ - `current_account`
24
+ - `current_user`
25
+ - `current_team`
26
+ - `current_company`
27
+ - `current_workspace`
28
+ - `current_tenant`
29
+ - If you set `plan_owner_class` in `pricing_plans.rb`, we’ll also try `current_<plan_owner_class>`.
30
+
31
+ If these methods are already defined in your Application Controller or individual controller(s), there's nothing you need to do! For example: `pricing_plans` works out of the box with Devise, because Devise already defines `current_user` at the Application Controller level.
32
+
33
+ If none of those methods are defined, or if you want custom logic, we recommend defining / overriding the method in your `ApplicationController`:
34
+ ```ruby
35
+ class ApplicationController < ActionController::Base
36
+ # Adapt to your auth/session logic
37
+ def current_organization
38
+ # Your lookup here (e.g., current_user.organization)
39
+ end
40
+ end
41
+ ```
42
+
43
+ If needed, you can override the plan owner per controller:
44
+ ```ruby
45
+ class YourSpecificController < ApplicationController
46
+ pricing_plans_plan_owner :current_organization
47
+
48
+ # Or pass a block:
49
+ # pricing_plans_plan_owner { current_user&.organization }
50
+ end
51
+ ```
52
+
53
+ Once all of this is configured, you can gate features and enforce limits easily in your controllers.
54
+
55
+ ## Gate features in controllers
56
+
57
+ Feature-gate any controller action with:
58
+
59
+ ```ruby
60
+ before_action { gate_feature!(:api_access) }
61
+ ```
62
+
63
+ You can also specify the plan owner to override global or per-controller settings:
64
+
65
+ ```ruby
66
+ before_action { gate_feature!(:api_access, plan_owner: current_organization) }
67
+ ```
68
+
69
+ We also provide syntactic sugar for each feature defined in your pricing plans. For example, if you defined `allows :api_access` in your plans, you can simply enforce it like this instead:
70
+
71
+ ```ruby
72
+ before_action :enforce_api_access!
73
+ ```
74
+
75
+ You can use it along with any other controller filters too:
76
+
77
+ ```ruby
78
+ before_action :enforce_api_access!, only: [:create]
79
+ ```
80
+
81
+ These `enforce_<feature_key>!` controller helper methods are dynamically generated for each of the features `<feature_key>` you defined in your plans. So, for the helper above to work, you would have to have defined a plan with `allows :api_access` in your `pricing_plans.rb` file.
82
+
83
+ When the feature is disallowed, the controller will raise a `FeatureDenied` (we rescue it by default). You can customize the response by overriding `handle_pricing_plans_feature_denied(error)` in your `ApplicationController`:
84
+
85
+ ```ruby
86
+ class ApplicationController < ActionController::Base
87
+ private
88
+
89
+ # Override the default 403 handler (optional)
90
+ def handle_pricing_plans_feature_denied(error)
91
+ # Custom HTML handling
92
+ redirect_to upgrade_path, alert: error.message, status: :see_other
93
+ end
94
+ end
95
+ ```
96
+
97
+ ## Enforce plan limits in controllers
98
+
99
+ You can enforce limits for any action:
100
+
101
+ ```ruby
102
+ before_action { enforce_plan_limit!(:projects) }
103
+ ```
104
+
105
+ You can also override who the plan owner is:
106
+
107
+ ```ruby
108
+ before_action { enforce_plan_limit!(:projects, plan_owner: current_organization) }
109
+ ```
110
+
111
+ As with feature gating, there is syntactic sugar per limit:
112
+
113
+ ```ruby
114
+ before_action :enforce_projects_limit!
115
+ ```
116
+
117
+ The pattern is `enforce_<limit_key>_limit!` -- a method gets generated for every different `<limit_key>` defined with the `limits` keyword in `pricing_plans.rb`.
118
+
119
+
120
+ You can also specify a custom redirect path that will override the global config:
121
+ ```ruby
122
+ before_action { enforce_plan_limit!(:projects, plan_owner: current_organization, redirect_to: pricing_path) }
123
+ ```
124
+
125
+ > [!IMPORTANT]
126
+ > Enforcing a plan limit means "checking if **one more** object can be created". That is the default behavior. If you need to check whether you are at distance 2, or distance _n_ from the limit, you can pass the `by` argument as described below.
127
+
128
+ In the example aboves, the gem assumes the action to call will only create one extra project. So, if the plan limit is 5, and you're currently at 4 projects, you can still create one extra one, and the action will get called. If your action creates more than one object per call (creating multiple objects at once, importing objects in bulk etc.) you can enforce it will stay within plan limits by passing the `by:` parameter like this:
129
+
130
+ ```ruby
131
+ before_action { enforce_projects_limit!(by: 10) } # Checks whether current_organization can create 10 more projects within its plan limits
132
+ ```
133
+
134
+ ### Getting the raw `result` from a limit check
135
+
136
+ The `require_plan_limit!` method is also available (`require_`, not `enforce_`). This method returns a raw `result` object which is the result of checking the limit with respect to the current plan owner. You can call these on `result`:
137
+ - `result.message`
138
+ - `result.ok?`
139
+ - `result.warning?`
140
+ - `result.grace?`
141
+ - `result.blocked?`
142
+ - `result.success?`
143
+
144
+ This is useful for checking and enforcing limits mid-action (rather than via a `before_action` hook):
145
+
146
+ ```ruby
147
+ def create
148
+ result = require_plan_limit!(:products, plan_owner: current_organization, by: 1)
149
+
150
+ if result.blocked? # ok?, warning?, grace?, blocked?, success?
151
+ # result.message is available:
152
+ redirect_to pricing_path, alert: result.message, status: :see_other and return
153
+ end
154
+
155
+ # ...
156
+ Product.create!(...)
157
+ redirect_to products_path
158
+ end
159
+ ```
160
+
161
+ You can also define how your application responds when a limit check blocks an action by defining `handle_pricing_plans_limit_blocked` in your controller:
162
+
163
+ ```ruby
164
+ class ApplicationController < ActionController::Base
165
+ private
166
+
167
+ def handle_pricing_plans_limit_blocked(result)
168
+ # Default behavior (HTML): flash + redirect_to(pricing_path) if defined; else render 403
169
+ # You can customize globally here. The Result carries rich context:
170
+ # - result.limit_key, result.plan_owner, result.message, result.metadata
171
+ redirect_to(pricing_path, status: :see_other, alert: result.message)
172
+ end
173
+ end
174
+ ```
175
+
176
+ `enforce_plan_limit!` invokes this handler when `result.blocked?`, passing a `Result` enriched with `metadata[:redirect_to]` resolved via:
177
+ 1. explicit `redirect_to:` option
178
+ 2. per-controller default `self.pricing_plans_redirect_on_blocked_limit`
179
+ 3. global `config.redirect_on_blocked_limit`
180
+ 4. `pricing_path` helper if available
181
+
182
+
183
+ ## Set up a redirect when a limit is reached
184
+
185
+ You can optionally configure a global default redirect:
186
+
187
+ ```ruby
188
+ # config/initializers/pricing_plans.rb
189
+ PricingPlans.configure do |config|
190
+ config.redirect_on_blocked_limit = :pricing_path # or "/pricing" or ->(result) { pricing_path }
191
+ end
192
+ ```
193
+
194
+ Or a per-controller default (optional):
195
+
196
+ ```ruby
197
+ class ApplicationController < ActionController::Base
198
+ self.pricing_plans_redirect_on_blocked_limit = :pricing_path
199
+ end
200
+ ```
201
+
202
+ Redirect resolution priority:
203
+ 1) `redirect_to:` option on the call
204
+ 2) Per-controller `self.pricing_plans_redirect_on_blocked_limit`
205
+ 3) Global `config.redirect_on_blocked_limit`
206
+ 4) `pricing_path` helper (if present)
207
+ 5) Fallback: render 403 (HTML or JSON)
208
+
209
+ Per-controller default accepts:
210
+ - Symbol: helper method name (e.g., `:pricing_path`)
211
+ - String: path or URL (e.g., `"/pricing"`)
212
+ - Proc: `->(result) { pricing_path }` (instance-exec'd in the controller)
213
+
214
+ Global default accepts the same types. The Proc receives the `Result` so you can branch on `limit_key`, etc.
215
+
216
+ Recommended patterns:
217
+ - Set a single global default in your initializer.
218
+ - Override per controller only if UX differs for a section.
219
+ - Use the dynamic helpers as symbols in before_action for clarity:
220
+ ```ruby
221
+ before_action :enforce_projects_limit!, only: :create
222
+ before_action :enforce_api_access!
223
+ ```