pricing_plans 0.5.0 → 0.7.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -7
  3. data/.simplecov +6 -5
  4. data/Appraisals +5 -1
  5. data/CHANGELOG.md +34 -0
  6. data/README.md +64 -1
  7. data/docs/01-define-pricing-plans.md +6 -1
  8. data/docs/03-model-helpers.md +16 -0
  9. data/docs/06-gem-compatibility.md +35 -0
  10. data/docs/07-repricing.md +103 -0
  11. data/docs/08-feature-passes.md +192 -0
  12. data/gemfiles/rails_7.1.gemfile +26 -0
  13. data/gemfiles/rails_7.2.gemfile +3 -2
  14. data/gemfiles/rails_8.1.gemfile +2 -1
  15. data/lib/generators/pricing_plans/grants/grants_generator.rb +43 -0
  16. data/lib/generators/pricing_plans/grants/templates/create_pricing_plans_feature_grants.rb.erb +44 -0
  17. data/lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb +29 -8
  18. data/lib/generators/pricing_plans/install/templates/initializer.rb +11 -0
  19. data/lib/generators/pricing_plans/passes/passes_generator.rb +31 -0
  20. data/lib/generators/pricing_plans/passes/templates/add_feature_pass_limits.rb.erb +19 -0
  21. data/lib/pricing_plans/engine.rb +1 -0
  22. data/lib/pricing_plans/feature_access.rb +110 -0
  23. data/lib/pricing_plans/models/assignment.rb +2 -4
  24. data/lib/pricing_plans/models/feature_grant.rb +212 -0
  25. data/lib/pricing_plans/overage_reporter.rb +7 -1
  26. data/lib/pricing_plans/pay_support.rb +92 -100
  27. data/lib/pricing_plans/plan.rb +113 -7
  28. data/lib/pricing_plans/plan_owner.rb +97 -7
  29. data/lib/pricing_plans/plan_owner_identity.rb +30 -0
  30. data/lib/pricing_plans/plan_resolver.rb +53 -37
  31. data/lib/pricing_plans/version.rb +1 -1
  32. data/lib/pricing_plans.rb +6 -1
  33. metadata +14 -4
@@ -19,7 +19,7 @@ module PricingPlans
19
19
  log_debug "[PricingPlans::PlanResolver] resolution_for called for #{plan_owner.class.name}##{plan_owner.respond_to?(:id) ? plan_owner.id : 'N/A'}"
20
20
 
21
21
  assignment = pricing_plan_override_for(plan_owner)
22
- subscription = current_subscription_for(plan_owner)
22
+ subscription = current_subscription_for(plan_owner, preferred_plan_key: assignment&.plan_key)
23
23
 
24
24
  if assignment
25
25
  log_debug "[PricingPlans::PlanResolver] Returning explicit-override resolution: #{assignment.plan_key}"
@@ -31,20 +31,8 @@ module PricingPlans
31
31
  )
32
32
  end
33
33
 
34
- if subscription
35
- processor_plan = subscription.processor_plan
36
- log_debug "[PricingPlans::PlanResolver] resolution_for subscription processor_plan = #{processor_plan.inspect}"
37
-
38
- if processor_plan && (plan = plan_from_processor_plan(processor_plan))
39
- log_debug "[PricingPlans::PlanResolver] Returning subscription-backed resolution: #{plan.key}"
40
- return PlanResolution.new(
41
- plan: plan,
42
- source: :subscription,
43
- assignment: nil,
44
- subscription: subscription
45
- )
46
- end
47
- end
34
+ subscription_resolution = subscription_resolution_for(subscription)
35
+ return subscription_resolution if subscription_resolution
48
36
 
49
37
  default = Registry.default_plan
50
38
  log_debug "[PricingPlans::PlanResolver] Returning default-backed resolution: #{default ? default.key : 'nil'}"
@@ -84,6 +72,20 @@ module PricingPlans
84
72
  )
85
73
  end
86
74
 
75
+ # Resolve a processor price identifier to the configured plan that owns
76
+ # it. Public so entitlement provenance can verify that an underlying
77
+ # subscription belongs to the same plan as a manual override.
78
+ def plan_for_processor_plan(processor_plan)
79
+ return nil if processor_plan.blank?
80
+
81
+ Registry.plans.values.find do |plan|
82
+ stripe_price = plan.stripe_price
83
+ next unless stripe_price
84
+
85
+ stripe_price.is_a?(Hash) ? stripe_price.value?(processor_plan) : stripe_price == processor_plan
86
+ end
87
+ end
88
+
87
89
  private
88
90
 
89
91
  # Backward-compatible shim for tests that stub pay_available?
@@ -96,8 +98,7 @@ module PricingPlans
96
98
  return nil unless plan_owner.respond_to?(:id)
97
99
 
98
100
  assignment = Assignment.find_by(
99
- plan_owner_type: plan_owner.class.name,
100
- plan_owner_id: plan_owner.id
101
+ PlanOwnerIdentity.conditions_for(plan_owner)
101
102
  )
102
103
 
103
104
  if assignment
@@ -109,7 +110,24 @@ module PricingPlans
109
110
  assignment
110
111
  end
111
112
 
112
- def current_subscription_for(plan_owner)
113
+ def subscription_resolution_for(subscription)
114
+ return nil unless subscription.respond_to?(:processor_plan)
115
+
116
+ processor_plan = subscription.processor_plan
117
+ log_debug "[PricingPlans::PlanResolver] resolution_for subscription processor_plan = #{processor_plan.inspect}"
118
+ plan = plan_for_processor_plan(processor_plan)
119
+ return nil unless plan
120
+
121
+ log_debug "[PricingPlans::PlanResolver] Returning subscription-backed resolution: #{plan.key}"
122
+ PlanResolution.new(
123
+ plan: plan,
124
+ source: :subscription,
125
+ assignment: nil,
126
+ subscription: subscription
127
+ )
128
+ end
129
+
130
+ def current_subscription_for(plan_owner, preferred_plan_key: nil)
113
131
  return nil unless plan_owner
114
132
 
115
133
  pay_available = pay_available?
@@ -117,32 +135,30 @@ module PricingPlans
117
135
 
118
136
  return nil unless pay_available
119
137
 
120
- # This intentionally delegates the active/trial/grace filtering contract
121
- # to PaySupport.current_subscription_for so resolution_for can preserve
122
- # the same billing context wherever it is called.
123
- subscription = PaySupport.current_subscription_for(plan_owner)
138
+ subscriptions = PaySupport.current_subscriptions_for(plan_owner)
139
+ subscription = select_subscription(subscriptions, preferred_plan_key: preferred_plan_key)
124
140
  log_debug "[PricingPlans::PlanResolver] current_subscription_for returned: #{subscription ? subscription.class.name : 'nil'}"
125
141
  subscription
126
142
  end
127
143
 
128
- def plan_from_processor_plan(processor_plan)
129
- # Look through all plans to find one matching this Stripe price
130
- Registry.plans.values.find do |plan|
131
- stripe_price = plan.stripe_price
132
- next unless stripe_price
144
+ def select_subscription(subscriptions, preferred_plan_key: nil)
145
+ subscriptions = Array(subscriptions)
133
146
 
134
- case stripe_price
135
- when String
136
- stripe_price == processor_plan
137
- when Hash
138
- stripe_price[:id] == processor_plan ||
139
- stripe_price[:month] == processor_plan ||
140
- stripe_price[:year] == processor_plan ||
141
- stripe_price.values.include?(processor_plan)
142
- else
143
- false
147
+ if preferred_plan_key
148
+ preferred = subscriptions.find do |subscription|
149
+ subscription_plan = plan_for_subscription(subscription)
150
+ subscription_plan&.key == preferred_plan_key.to_sym
144
151
  end
152
+ return preferred if preferred
145
153
  end
154
+
155
+ subscriptions.find { |subscription| plan_for_subscription(subscription) } || subscriptions.first
156
+ end
157
+
158
+ def plan_for_subscription(subscription)
159
+ return nil unless subscription.respond_to?(:processor_plan)
160
+
161
+ plan_for_processor_plan(subscription.processor_plan)
146
162
  end
147
163
  end
148
164
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PricingPlans
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/pricing_plans.rb CHANGED
@@ -30,6 +30,9 @@ module PricingPlans
30
30
  end
31
31
  class InvalidOperation < Error; end
32
32
 
33
+ autoload :FeatureAccess, "pricing_plans/feature_access"
34
+ autoload :FeatureLimitExceeded, "pricing_plans/feature_access"
35
+ autoload :FeatureGrantConflict, "pricing_plans/feature_access"
33
36
  autoload :Configuration, "pricing_plans/configuration"
34
37
  autoload :Registry, "pricing_plans/registry"
35
38
  autoload :Plan, "pricing_plans/plan"
@@ -37,6 +40,7 @@ module PricingPlans
37
40
  autoload :DSL, "pricing_plans/dsl"
38
41
  autoload :IntegerRefinements, "pricing_plans/integer_refinements"
39
42
  autoload :PlanResolver, "pricing_plans/plan_resolver"
43
+ autoload :PlanOwnerIdentity, "pricing_plans/plan_owner_identity"
40
44
  autoload :LegacyPlanAssignmentApi, "pricing_plans/legacy_plan_assignment_api"
41
45
  autoload :PaySupport, "pricing_plans/pay_support"
42
46
  autoload :LimitChecker, "pricing_plans/limit_checker"
@@ -61,6 +65,7 @@ module PricingPlans
61
65
  autoload :EnforcementState, "pricing_plans/models/enforcement_state"
62
66
  autoload :Usage, "pricing_plans/models/usage"
63
67
  autoload :Assignment, "pricing_plans/models/assignment"
68
+ autoload :FeatureGrant, "pricing_plans/models/feature_grant"
64
69
 
65
70
  class << self
66
71
  attr_writer :configuration
@@ -73,7 +78,7 @@ module PricingPlans
73
78
  # warnings independently and lets Rails include them in its deprecator
74
79
  # collection.
75
80
  def deprecator
76
- @deprecator ||= ActiveSupport::Deprecation.new("0.6.0", "pricing_plans")
81
+ @deprecator ||= ActiveSupport::Deprecation.new("0.8.0", "pricing_plans")
77
82
  end
78
83
 
79
84
  def configure(&block)
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.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-09-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -77,15 +77,22 @@ files:
77
77
  - docs/04-views.md
78
78
  - docs/05-semantic-pricing.md
79
79
  - docs/06-gem-compatibility.md
80
+ - docs/07-repricing.md
81
+ - docs/08-feature-passes.md
80
82
  - docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg
81
83
  - docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg
82
84
  - docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg
83
85
  - docs/images/product_creation_blocked.jpg
86
+ - gemfiles/rails_7.1.gemfile
84
87
  - gemfiles/rails_7.2.gemfile
85
88
  - gemfiles/rails_8.1.gemfile
89
+ - lib/generators/pricing_plans/grants/grants_generator.rb
90
+ - lib/generators/pricing_plans/grants/templates/create_pricing_plans_feature_grants.rb.erb
86
91
  - lib/generators/pricing_plans/install/install_generator.rb
87
92
  - lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb
88
93
  - lib/generators/pricing_plans/install/templates/initializer.rb
94
+ - lib/generators/pricing_plans/passes/passes_generator.rb
95
+ - lib/generators/pricing_plans/passes/templates/add_feature_pass_limits.rb.erb
89
96
  - lib/pricing_plans.rb
90
97
  - lib/pricing_plans/association_limit_registry.rb
91
98
  - lib/pricing_plans/callbacks.rb
@@ -95,6 +102,7 @@ files:
95
102
  - lib/pricing_plans/dsl.rb
96
103
  - lib/pricing_plans/engine.rb
97
104
  - lib/pricing_plans/exceeded_state_utils.rb
105
+ - lib/pricing_plans/feature_access.rb
98
106
  - lib/pricing_plans/grace_manager.rb
99
107
  - lib/pricing_plans/integer_refinements.rb
100
108
  - lib/pricing_plans/job_guards.rb
@@ -103,12 +111,14 @@ files:
103
111
  - lib/pricing_plans/limitable.rb
104
112
  - lib/pricing_plans/models/assignment.rb
105
113
  - lib/pricing_plans/models/enforcement_state.rb
114
+ - lib/pricing_plans/models/feature_grant.rb
106
115
  - lib/pricing_plans/models/usage.rb
107
116
  - lib/pricing_plans/overage_reporter.rb
108
117
  - lib/pricing_plans/pay_support.rb
109
118
  - lib/pricing_plans/period_calculator.rb
110
119
  - lib/pricing_plans/plan.rb
111
120
  - lib/pricing_plans/plan_owner.rb
121
+ - lib/pricing_plans/plan_owner_identity.rb
112
122
  - lib/pricing_plans/plan_resolution.rb
113
123
  - lib/pricing_plans/plan_resolver.rb
114
124
  - lib/pricing_plans/price_components.rb
@@ -123,7 +133,7 @@ licenses:
123
133
  - MIT
124
134
  metadata:
125
135
  homepage_uri: https://github.com/rameerez/pricing_plans
126
- source_code_uri: https://github.com/rameerez/pricing_plans
136
+ source_code_uri: https://github.com/rameerez/pricing_plans/tree/v0.7.0
127
137
  changelog_uri: https://github.com/rameerez/pricing_plans/blob/main/CHANGELOG.md
128
138
  bug_tracker_uri: https://github.com/rameerez/pricing_plans/issues
129
139
  documentation_uri: https://github.com/rameerez/pricing_plans#readme
@@ -142,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
152
  - !ruby/object:Gem::Version
143
153
  version: '0'
144
154
  requirements: []
145
- rubygems_version: 3.6.9
155
+ rubygems_version: 3.6.2
146
156
  specification_version: 4
147
157
  summary: Define and enforce pricing plan limits (entitlements, quotas, feature gating)
148
158
  in your Rails SaaS