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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6909ac57e070c2504657a4fd03d4be65c320dfde3a06458e5daa36ee81ba953a
4
+ data.tar.gz: ff00984bc91cac2e156af5909434e6fed135c5feb06d8047187edcb81d2e1a9a
5
+ SHA512:
6
+ metadata.gz: d8dc596158a482b5d466a3ad8bc2d6bbcced8639da88241a65b201acff9a9cbd5080aa6cfd6e0f317c8f558219973d63847cf9c0d3d9cca81b4d6f020740bb7c
7
+ data.tar.gz: 5827c8e721f641ed179c6bd42ade5c18c30a299099ae21e8af8eac285d886b311a896bba43636ed7fb2bdf698531839d471a66d4a94676787741b458afe48f18
@@ -0,0 +1,16 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(mkdir:*)",
5
+ "Bash(bundle install:*)",
6
+ "Bash(bundle exec rake:*)",
7
+ "Bash(bundle update:*)",
8
+ "Bash(bundle exec ruby:*)",
9
+ "Bash(sed:*)",
10
+ "Bash(grep:*)",
11
+ "Bash(ruby:*)",
12
+ "Bash(find:*)"
13
+ ],
14
+ "deny": []
15
+ }
16
+ }
data/.rubocop.yml ADDED
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require:
4
+ - rubocop-minitest
5
+ - rubocop-performance
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 3.2
9
+ NewCops: enable
10
+ Exclude:
11
+ - 'bin/**/*'
12
+ - 'vendor/**/*'
13
+ - 'test/dummy/**/*'
14
+ - 'db/migrate/**/*' # Generated migrations
15
+ - 'lib/generators/**/templates/**/*' # Generator templates
16
+
17
+ # Layout & Formatting
18
+ Layout/LineLength:
19
+ Max: 120
20
+ AllowedPatterns:
21
+ - '\s*#.*' # Allow long comments
22
+ - '^\s*raise\s' # Allow long raise statements
23
+
24
+ Layout/MultilineMethodCallIndentation:
25
+ EnforcedStyle: indented
26
+
27
+ Layout/ArgumentAlignment:
28
+ EnforcedStyle: with_first_argument
29
+
30
+ Layout/FirstArgumentIndentation:
31
+ EnforcedStyle: consistent
32
+
33
+ # Style
34
+ Style/Documentation:
35
+ Enabled: false # Don't require class documentation for now
36
+
37
+ Style/StringLiterals:
38
+ EnforcedStyle: double_quotes
39
+
40
+ Style/FrozenStringLiteralComment:
41
+ Enabled: true
42
+ EnforcedStyle: always
43
+
44
+ Style/ClassAndModuleChildren:
45
+ EnforcedStyle: compact
46
+
47
+ Style/GuardClause:
48
+ MinBodyLength: 3
49
+
50
+ # Metrics
51
+ Metrics/ClassLength:
52
+ Max: 150
53
+
54
+ Metrics/ModuleLength:
55
+ Max: 150
56
+
57
+ Metrics/MethodLength:
58
+ Max: 25
59
+ AllowedMethods:
60
+ - 'configure' # Configuration blocks can be longer
61
+
62
+ Metrics/BlockLength:
63
+ Max: 25
64
+ AllowedMethods:
65
+ - 'configure'
66
+ - 'describe'
67
+ - 'context'
68
+ - 'it'
69
+ - 'test'
70
+ Exclude:
71
+ - 'test/**/*' # Allow long test blocks
72
+
73
+ Metrics/AbcSize:
74
+ Max: 20
75
+ AllowedMethods:
76
+ - 'configure'
77
+
78
+ Metrics/CyclomaticComplexity:
79
+ Max: 8
80
+
81
+ Metrics/PerceivedComplexity:
82
+ Max: 10
83
+
84
+ # Naming
85
+ Naming/PredicateName:
86
+ ForbiddenPrefixes:
87
+ - 'is_'
88
+ AllowedMethods:
89
+ - 'is_a?'
90
+
91
+ # Performance
92
+ Performance/StringReplacement:
93
+ Enabled: true
94
+
95
+ Performance/RedundantMerge:
96
+ Enabled: true
97
+
98
+ # Minitest
99
+ Minitest/MultipleAssertions:
100
+ Enabled: false # Allow multiple assertions in integration tests
101
+
102
+ Minitest/AssertTruthy:
103
+ Enabled: false # Allow assert instead of assert_equal true
104
+
105
+ # Rails-specific (even though we don't have full Rails)
106
+ Style/Rails/HttpStatus:
107
+ Enabled: false
108
+
109
+ # Custom overrides for this gem
110
+ Style/AccessorGrouping:
111
+ Enabled: false # Allow separate attr_reader/attr_writer
112
+
113
+ Style/MutableConstant:
114
+ Enabled: false # We have some intentionally mutable constants
115
+
116
+ # Allow class variables for registry pattern
117
+ Style/ClassVars:
118
+ Enabled: false
119
+
120
+ # Allow metaprogramming patterns common in Rails engines
121
+ Style/EvalWithLocation:
122
+ Enabled: false
123
+
124
+ Lint/MissingSuper:
125
+ Enabled: false # Allow classes that don't call super
126
+
127
+ # Disable some cops that don't work well with our DSL
128
+ Style/MethodCallWithoutArgsParentheses:
129
+ Enabled: false # Our DSL looks better without parens
130
+
131
+ # Thread safety
132
+ Style/GlobalVars:
133
+ AllowedVariables: ['$0'] # Only allow program name
134
+
135
+ # Database-related
136
+ Style/NumericLiterals:
137
+ Enabled: false # Allow raw numbers in database IDs/amounts
data/CHANGELOG.md ADDED
@@ -0,0 +1,83 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-08-07
11
+
12
+ ### Added
13
+
14
+ **Core Features**
15
+ - Plan catalog configuration system with English-first DSL
16
+ - Feature flags (boolean allows/disallows)
17
+ - Persistent caps (max concurrent resources like projects, seats)
18
+ - Per-period discrete allowances (e.g., "3 custom models/month")
19
+ - Grace period enforcement with configurable behaviors
20
+ - Event system for warning/grace/block notifications
21
+
22
+ **Configuration & DSL**
23
+ - `PricingPlans.configure` block for one-file configuration
24
+ - Plan definition with name, description, bullets, pricing
25
+ - `Integer#max` refinement for clean DSL (`5.max`)
26
+ - Support for Stripe price IDs and manual pricing
27
+ - Flexible period cycles (billing, calendar month/week/day, custom)
28
+
29
+ **Database & Models**
30
+ - Three-table schema for enforcement states, usage counters, assignments
31
+ - `EnforcementState` model for grace period tracking with row-level locking
32
+ - `Usage` model for per-period counters with atomic upserts
33
+ - `Assignment` model for manual plan overrides
34
+
35
+ **Controller Integration**
36
+ - `require_plan_limit!` guard returning rich Result objects
37
+ - `require_feature!` guard with FeatureDenied exception
38
+ - Automatic grace period management and event emission
39
+ - Race-safe limit checking with retries
40
+
41
+ **Model Integration**
42
+ - `Limitable` mixin for ActiveRecord models
43
+ - `limited_by` macro for automatic usage tracking
44
+ - Real-time persistent caps (no counter caches needed)
45
+ - Automatic per-period counter increments
46
+
47
+ **View Helpers & UI**
48
+ - Complete pricing table rendering
49
+ - Usage meters with progress bars
50
+ - Limit banners with warnings/grace/blocked states
51
+ - Plan information helpers (current plan, feature checks)
52
+
53
+ **Pay Integration**
54
+ - Automatic plan resolution from Stripe subscriptions
55
+ - Support for trial, grace, and active subscription states
56
+ - Price ID to plan mapping
57
+ - Billing cycle anchor integration for periods
58
+
59
+ **usage_credits Integration**
60
+ - Credit inclusion display in pricing tables
61
+ - Boot-time linting to prevent limit/credit collisions
62
+ - Operation validation against usage_credits registry
63
+ - Clean separation of concerns (credits vs discrete limits)
64
+
65
+ **Generators**
66
+ - Install generator with migrations and initializer template
67
+ - Pricing generator with views, controller, and CSS
68
+ - Comprehensive Tailwind-friendly styling
69
+
70
+ **Architecture & Performance**
71
+ - Rails Engine for seamless integration
72
+ - Autoloading with proper namespacing
73
+ - Row-level locking for race condition prevention
74
+ - Efficient query patterns with proper indexing
75
+ - Memoization and caching where appropriate
76
+
77
+ ### Technical Details
78
+
79
+ - Ruby 3.2+ requirement
80
+ - Rails 7.1+ requirement (ActiveRecord, ActiveSupport)
81
+ - PostgreSQL optimized (with fallbacks for other databases)
82
+ - Comprehensive error handling and validation
83
+ - Thread-safe implementation throughout
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Javi R
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # 💵 `pricing_plans` - Define and enforce pricing plan limits in your Rails app (SaaS entitlements)
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/pricing_plans.svg)](https://badge.fury.io/rb/pricing_plans)
4
+
5
+ Enforce pricing plan limits with one-liners that read like plain English. Avoid scattering and entangling pricing logic everywhere in your Rails SaaS.
6
+
7
+ For example, this is how you define pricing plans and their entitlements:
8
+ ```ruby
9
+ plan :pro do
10
+ allows :api_access
11
+ limits :projects, to: 5
12
+ end
13
+ ```
14
+
15
+ You can then gate features in your controllers:
16
+ ```ruby
17
+ before_action :enforce_api_access!, only: [:create]
18
+ ```
19
+
20
+ Do one-liner checks to hide / show conditional UI:
21
+
22
+ ```ruby
23
+ <% if current_user.within_plan_limits?(:projects) %>
24
+ ...
25
+ <% end %>
26
+ ```
27
+
28
+ Or check limits and feature access anywhere in your app:
29
+
30
+ ```ruby
31
+ @user.plan_allows_api_access? # => true / false
32
+ @user.projects_remaining # => 2
33
+ ```
34
+
35
+ `pricing_plans` is your single source of truth for pricing plans, so you can use it to [build pricing pages and paywalls](/docs/04-views.md) too.
36
+
37
+ ![pricing_plans Ruby on Rails gem - pricing table features](/docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg)
38
+
39
+
40
+ The gem works standalone, and it also plugs nicely into popular gems: it works seamlessly out of the box if you're already using [`pay`](https://github.com/pay-rails/pay) or [`usage_credits`](https://github.com/rameerez/usage_credits/). More info [here](/docs/06-gem-compatibility.md).
41
+
42
+ ## Quickstart
43
+
44
+ Add this to your Gemfile:
45
+
46
+ ```ruby
47
+ gem "pricing_plans"
48
+ ```
49
+
50
+ Then install the gem:
51
+
52
+ ```bash
53
+ bundle install
54
+ ```
55
+
56
+ After that, generate and run [the required migration](#why-the-models):
57
+
58
+ ```bash
59
+ rails g pricing_plans:install
60
+ rails db:migrate
61
+ ```
62
+
63
+ This will also create a `config/initializers/pricing_plans.rb` file where you need to [define your pricing plans](/docs/01-define-pricing-plans.md).
64
+
65
+ Then, just add the model mixin to the plan owner, that is: the actual model on which limits should be enforced (`User`, `Organization`, etc.):
66
+
67
+ ```ruby
68
+ class User < ApplicationRecord
69
+ include PricingPlans::PlanOwner
70
+ end
71
+ ```
72
+
73
+ This mixin will automatically give your plan owner model the [model helpers and methods](/docs/03-model-helpers.md) you can use to consistently check and enforce limits:
74
+ ```ruby
75
+ class User < ApplicationRecord
76
+ include PricingPlans::PlanOwner
77
+
78
+ has_many :projects, limited_by_pricing_plans: { error_after_limit: "Too many projects for your plan!" }, dependent: :destroy
79
+ end
80
+ ```
81
+
82
+ You also get [controller helpers](/docs/02-controller-helpers.md):
83
+
84
+ ```ruby
85
+ before_action { gate_feature!(:api_access) }
86
+
87
+ # or with syntactic sugar:
88
+
89
+ before_action :enforce_api_access!
90
+ ```
91
+
92
+ And you also get a lot of [view helpers and methods](/docs/04-views.md) to check limits in your views for conditional UI, and to build usage meters, usage warnings, and a handful of other useful UI components.
93
+
94
+ ![pricing_plans Ruby on Rails gem - pricing plan usage meter](/docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg)
95
+
96
+ You can also display upgrade alerts to prompt users into upgrading to the next plan when they're near their plan limits:
97
+
98
+ ![pricing_plans Ruby on Rails gem - pricing plan upgrade prompt](/docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg)
99
+
100
+
101
+ ## 🤓 Read the docs!
102
+
103
+ > [!IMPORTANT]
104
+ > This gem has extensive docs. Please 👉 [read the docs here](/docs/01-define-pricing-plans.md) 👈
105
+
106
+ ## What `pricing_plans` does and doesn't do
107
+
108
+ `pricing_plans` handles pricing plan entitlements; that is: what a user can and can't access based on their current SaaS plan.
109
+
110
+ Some other features you may like:
111
+ - Grace periods (hard & soft caps for limits)
112
+ - Customizable downgrade behavior for overage handling
113
+ - Row-level locks to prevent race conditions on quota enforcement
114
+
115
+ Here's what `pricing_plans` **does not** handle:
116
+ - Payment processing / billing (that's [`pay`](https://github.com/pay-rails/pay) or Stripe's responsibility)
117
+ - Price definition / currency handling (that's Stripe / payment processor)
118
+ - Usage credits / metered usage (that's [`usage_credits`](https://github.com/rameerez/usage_credits/)'s responsibility)
119
+ - Feature flags for A/B testing or staged rollouts (that's `flipper`)
120
+ - User roles, authorization, or per-user permissions (that's `cancancan` or `pundit`)
121
+
122
+ ## 🤔 Why this gem exists
123
+
124
+ If you've ever had to implement pricing plan limits, you probably found yourself writing code like this everywhere in your app:
125
+
126
+ ```ruby
127
+ if user_signed_in? && current_user.payment_processor&.subscription&.processor_plan == "pro" && current_user.projects.count <= 5
128
+ # ...
129
+ elsif user_signed_in? && current_user.payment_processor&.subscription&.processor_plan == "premium" && current_user.projects.count <= 10
130
+ # ...
131
+ end
132
+ ```
133
+
134
+ You end up duplicating this kind of snippet for every plan and feature, and for every view and controller.
135
+
136
+ This code is brittle, tends to be full of magical numbers and nested convoluted logic; and plan enforcement tends to be scattered across the entire codebase. If you change something in your pricing table, it's highly likely you'll have to change the same magical number or logic in many different places, leading to bugs, inconsistencies, customer support tickets, and maintenance hell.
137
+
138
+ Enforcing pricing plan limits in code (through entitlements, usage quotas, and feature gating) is tedious and painful plumbing. Every SaaS needs to check whether users can perform an action based on the plan they're currently subscribed to, but it often leads to brittle, scattered, unmaintainable pricing logic that gets entangled with core application code, opening gaps for under-enforcement and leaving money on the table.
139
+
140
+ Integrating payment processing (Stripe, `pay`, etc.) is relatively straightforward, but enforcing actual plan limits (ensure users only get the features and usage their tier allows) is a whole different task. It's the kind of plumbing no one wants to do. Founders often put their focus on capturing the payment, and then default to a "poor man's" implementation of per-plan entitlements. Maintaining these in-house DIY solutions is a huge time sink, and engineers often can't keep up with constant pricing or packaging changes.
141
+
142
+ `pricing_plans` aims to offer a centralized, single-source-of-truth way of defining & handling pricing plans, so you can enforce plan limits with reusable helpers that read like plain English.
143
+
144
+
145
+ ## Why the models?
146
+
147
+ The `pricing_plans` gem needs three new models in the schema in order to work: `Assignment`, `EnforcementState`, and `Usage`. Why are they needed?
148
+
149
+ - `PricingPlans::Assignment` allow manual plan overrides independent of billing system (or before you wire up Stripe/Pay). Great for admin toggles, trials, demos.
150
+ - What: The arbitrary `plan_key` and a `source` label (default "manual"). Unique per plan_owner.
151
+ - How it’s used: `PlanResolver` checks Pay → manual assignment → default plan. You can call `assign_pricing_plan!` and `remove_pricing_plan!` on the plan_owner.
152
+
153
+ - `PricingPlans::EnforcementState` tracks per-plan_owner per-limit enforcement state for persistent caps and per-period allowances (grace/warnings/block state) in a race-safe way.
154
+ - What: `exceeded_at`, `blocked_at`, last warning info, and a small JSON `data` column where we persist plan-derived parameters like grace period seconds.
155
+ - How it’s used: When you exceed a limit, we upsert/read this row under row-level locking to start grace, compute when it ends, flip to blocked, and to ensure idempotent event emission (`on_warning`, `on_grace_start`, `on_block`).
156
+
157
+ - `PricingPlans::Usage` tracks per-period allowances (e.g., “3 projects per month”). Persistent caps don’t need a table because they are live counts.
158
+ - What: `period_start`, `period_end`, and a monotonic `used` counter with a last-used timestamp.
159
+ - How it’s used: On create of the metered model, we increment or upsert the usage for the current window (based on `PeriodCalculator`). Reads power `remaining`, `percent_used`, and warning thresholds.
160
+
161
+ ## Gem features
162
+
163
+ Enforcing pricing plans is one of those boring plumbing problems that look easy from a distance but get complex when you try to engineer them for production usage. The poor man's implementation of nested ifs shown in the example above only get you so far, you soon start finding edge cases to consider. Here's some of what we've covered in this gem:
164
+
165
+ - Safe under load: we use row locks and retries when setting grace/blocked/warning state, and we avoid firing the same event twice. See [grace_manager.rb](lib/pricing_plans/grace_manager.rb).
166
+
167
+ - Accurate counting: persistent limits count live current rows (using `COUNT(*)`, make sure to index your foreign keys to make it fast at scale); per‑period limits record usage for the current window only. You can filter what counts with `count_scope` (Symbol/Hash/Proc/Array), and plan settings override model defaults. See [limitable.rb](lib/pricing_plans/limitable.rb) and [limit_checker.rb](lib/pricing_plans/limit_checker.rb).
168
+
169
+ - Clear rules: default is to block when you hit the cap; grace periods are opt‑in. In status/UI, 0 of 0 isn’t shown as blocked. See [plan.rb](lib/pricing_plans/plan.rb), [grace_manager.rb](lib/pricing_plans/grace_manager.rb), and [view_helpers.rb](lib/pricing_plans/view_helpers.rb).
170
+
171
+ - Simple controllers: one‑liners to guard actions, predictable redirect order (per‑call → per‑controller → global → pricing_path), and an optional central handler. See [controller_guards.rb](lib/pricing_plans/controller_guards.rb).
172
+
173
+ - Billing‑aware periods: supports billing cycle (when Pay is present), calendar month/week/day, custom time windows, and durations. See [period_calculator.rb](lib/pricing_plans/period_calculator.rb).
174
+
175
+
176
+ ## Downgrades and overages
177
+
178
+ When a customer moves to a lower plan (via Stripe/Pay or manual assignment), the new plan’s limits start applying immediately. Existing resources are never auto‑deleted by the gem; instead:
179
+
180
+ - **Persistent caps** (e.g., `:projects, to: 3`): We count live rows. If the account is now over the new cap, creations will be blocked (or put into grace/warn depending on `after_limit`). Users must remediate by deleting/archiving until under cap.
181
+ -
182
+ - **Per‑period allowances** (e.g., `:custom_models, to: 3, per: :month`): The current window’s usage remains as is. Further creations in the same window respect the downgraded allowance and `after_limit` policy. At the next window, the allowance resets.
183
+
184
+ Use `OverageReporter` to present a clear remediation UX before or after applying a downgrade:
185
+
186
+ ```ruby
187
+ report = PricingPlans::OverageReporter.report_with_message(org, :free)
188
+ if report.items.any?
189
+ flash[:alert] = report.message
190
+ # report.items -> [#<OverageItem limit_key:, kind: :persistent|:per_period, current_usage:, allowed:, overage:, grace_active:, grace_ends_at:>]
191
+ end
192
+ ```
193
+
194
+ Example human message:
195
+ - "Over target plan on: projects: 12 > 3 (reduce by 9), custom_models: 5 > 0 (reduce by 5). Grace active — projects grace ends at 2025-01-06T12:00:00Z."
196
+
197
+ Notes:
198
+ - If you provide a `config.message_builder`, it’s used to customize copy for the `:overage_report` context.
199
+ - This reporter works regardless of whether any controller/model action has been hit; it reads live counts and current period usage.
200
+
201
+ ### Override checks
202
+
203
+ Some times you'll want to override plan limits / feature gating checks. A common use case is if you're responding to a webhook (like Stripe), you'll want to process the webhook correctly (bypassing the check) and maybe later handle the limit manually.
204
+
205
+ To do that, you can use `require_plan_limit!`. An example to proceed but mark downstream:
206
+
207
+ ```ruby
208
+ def webhook_create
209
+ result = require_plan_limit!(:projects, plan_owner: current_organization, allow_system_override: true)
210
+
211
+ # Your custom logic here.
212
+ # You could proceed to create; inspect result.grace?/warning? and result.metadata[:system_override]
213
+ Project.create!(metadata: { created_during_grace: result.grace? || result.warning?, system_override: result.metadata[:system_override] })
214
+
215
+ head :ok
216
+ end
217
+ ```
218
+
219
+ Note: model validations will still block creation even with `allow_system_override` -- it's just intended to bypass the block on controllers.
220
+
221
+ ## Testing
222
+
223
+ We use Minitest for testing. Run the test suite with:
224
+
225
+ ```bash
226
+ bundle exec rake test
227
+ ```
228
+
229
+ ## Development
230
+
231
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
232
+
233
+ To install this gem onto your local machine, run `bundle exec rake install`.
234
+
235
+ ## Contributing
236
+
237
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/pricing_plans. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
238
+
239
+ ## License
240
+
241
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "rubocop/rake_task"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: %i[test rubocop]