pricing_plans 0.5.0 → 0.6.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.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ # Builds the identity Rails stores for polymorphic plan-owner associations.
5
+ #
6
+ # Rails stores an Active Record model's polymorphic_name (normally its STI
7
+ # base class), not necessarily `record.class.name`. Keeping this in one place
8
+ # prevents reads and writes from disagreeing for STI subclasses and respects
9
+ # the host application's `store_full_class_name` configuration.
10
+ module PlanOwnerIdentity
11
+ module_function
12
+
13
+ def type_for(plan_owner_or_class)
14
+ plan_owner_class = plan_owner_or_class.is_a?(Class) ? plan_owner_or_class : plan_owner_or_class.class
15
+
16
+ if plan_owner_class.respond_to?(:polymorphic_name)
17
+ plan_owner_class.polymorphic_name
18
+ else
19
+ plan_owner_class.name
20
+ end
21
+ end
22
+
23
+ def conditions_for(plan_owner)
24
+ {
25
+ plan_owner_type: type_for(plan_owner),
26
+ plan_owner_id: plan_owner.respond_to?(:id) ? plan_owner.id : nil
27
+ }
28
+ end
29
+ end
30
+ end
@@ -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.6.0"
5
5
  end
data/lib/pricing_plans.rb CHANGED
@@ -37,6 +37,7 @@ module PricingPlans
37
37
  autoload :DSL, "pricing_plans/dsl"
38
38
  autoload :IntegerRefinements, "pricing_plans/integer_refinements"
39
39
  autoload :PlanResolver, "pricing_plans/plan_resolver"
40
+ autoload :PlanOwnerIdentity, "pricing_plans/plan_owner_identity"
40
41
  autoload :LegacyPlanAssignmentApi, "pricing_plans/legacy_plan_assignment_api"
41
42
  autoload :PaySupport, "pricing_plans/pay_support"
42
43
  autoload :LimitChecker, "pricing_plans/limit_checker"
@@ -61,6 +62,7 @@ module PricingPlans
61
62
  autoload :EnforcementState, "pricing_plans/models/enforcement_state"
62
63
  autoload :Usage, "pricing_plans/models/usage"
63
64
  autoload :Assignment, "pricing_plans/models/assignment"
65
+ autoload :FeatureGrant, "pricing_plans/models/feature_grant"
64
66
 
65
67
  class << self
66
68
  attr_writer :configuration
@@ -73,7 +75,7 @@ module PricingPlans
73
75
  # warnings independently and lets Rails include them in its deprecator
74
76
  # collection.
75
77
  def deprecator
76
- @deprecator ||= ActiveSupport::Deprecation.new("0.6.0", "pricing_plans")
78
+ @deprecator ||= ActiveSupport::Deprecation.new("0.7.0", "pricing_plans")
77
79
  end
78
80
 
79
81
  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.6.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-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -77,12 +77,16 @@ 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
80
81
  - docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg
81
82
  - docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg
82
83
  - docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg
83
84
  - docs/images/product_creation_blocked.jpg
85
+ - gemfiles/rails_7.1.gemfile
84
86
  - gemfiles/rails_7.2.gemfile
85
87
  - gemfiles/rails_8.1.gemfile
88
+ - lib/generators/pricing_plans/grants/grants_generator.rb
89
+ - lib/generators/pricing_plans/grants/templates/create_pricing_plans_feature_grants.rb.erb
86
90
  - lib/generators/pricing_plans/install/install_generator.rb
87
91
  - lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb
88
92
  - lib/generators/pricing_plans/install/templates/initializer.rb
@@ -103,12 +107,14 @@ files:
103
107
  - lib/pricing_plans/limitable.rb
104
108
  - lib/pricing_plans/models/assignment.rb
105
109
  - lib/pricing_plans/models/enforcement_state.rb
110
+ - lib/pricing_plans/models/feature_grant.rb
106
111
  - lib/pricing_plans/models/usage.rb
107
112
  - lib/pricing_plans/overage_reporter.rb
108
113
  - lib/pricing_plans/pay_support.rb
109
114
  - lib/pricing_plans/period_calculator.rb
110
115
  - lib/pricing_plans/plan.rb
111
116
  - lib/pricing_plans/plan_owner.rb
117
+ - lib/pricing_plans/plan_owner_identity.rb
112
118
  - lib/pricing_plans/plan_resolution.rb
113
119
  - lib/pricing_plans/plan_resolver.rb
114
120
  - lib/pricing_plans/price_components.rb
@@ -123,7 +129,7 @@ licenses:
123
129
  - MIT
124
130
  metadata:
125
131
  homepage_uri: https://github.com/rameerez/pricing_plans
126
- source_code_uri: https://github.com/rameerez/pricing_plans
132
+ source_code_uri: https://github.com/rameerez/pricing_plans/tree/v0.6.0
127
133
  changelog_uri: https://github.com/rameerez/pricing_plans/blob/main/CHANGELOG.md
128
134
  bug_tracker_uri: https://github.com/rameerez/pricing_plans/issues
129
135
  documentation_uri: https://github.com/rameerez/pricing_plans#readme
@@ -142,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
148
  - !ruby/object:Gem::Version
143
149
  version: '0'
144
150
  requirements: []
145
- rubygems_version: 3.6.9
151
+ rubygems_version: 3.6.2
146
152
  specification_version: 4
147
153
  summary: Define and enforce pricing plan limits (entitlements, quotas, feature gating)
148
154
  in your Rails SaaS